commit c8cebdfeeeb6bf575008655c820dffa6ea5eb76c Author: wehub-resource-sync Date: Mon Jul 13 13:11:39 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 0000000..f665c87 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,2720 @@ +{ + "projectName": "hertzbeat", + "projectOwner": "apache", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "README.md", + "README_CN.md", + "README_JP.md" + ], + "imageSize": 100, + "commit": true, + "commitConvention": "none", + "contributors": [ + { + "login": "tomsun28", + "name": "tomsun28", + "avatar_url": "https://avatars.githubusercontent.com/u/24788200?v=4", + "profile": "https://github.com/tomsun28", + "contributions": [ + "code", + "doc", + "design" + ] + }, + { + "login": "wang1027-wqh", + "name": "会编程的王学长", + "avatar_url": "https://avatars.githubusercontent.com/u/71161318?v=4", + "profile": "https://github.com/wang1027-wqh", + "contributions": [ + "code", + "doc", + "design" + ] + }, + { + "login": "shimingxy", + "name": "MaxKey", + "avatar_url": "https://avatars.githubusercontent.com/u/1563377?v=4", + "profile": "https://www.maxkey.top/", + "contributions": [ + "code", + "design", + "ideas" + ] + }, + { + "login": "gcdd1993", + "name": "观沧海", + "avatar_url": "https://avatars.githubusercontent.com/u/26523525?v=4", + "profile": "https://blog.gcdd.top/", + "contributions": [ + "code", + "design", + "bug" + ] + }, + { + "login": "a25017012", + "name": "yuye", + "avatar_url": "https://avatars.githubusercontent.com/u/32265356?v=4", + "profile": "https://github.com/a25017012", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "jx10086", + "name": "jx10086", + "avatar_url": "https://avatars.githubusercontent.com/u/5323228?v=4", + "profile": "https://github.com/jx10086", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "winnerTimer", + "name": "winnerTimer", + "avatar_url": "https://avatars.githubusercontent.com/u/76024658?v=4", + "profile": "https://github.com/winnerTimer", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "goo-kits", + "name": "goo-kits", + "avatar_url": "https://avatars.githubusercontent.com/u/13163673?v=4", + "profile": "https://github.com/goo-kits", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "brave4Time", + "name": "brave4Time", + "avatar_url": "https://avatars.githubusercontent.com/u/105094014?v=4", + "profile": "https://github.com/brave4Time", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "walkerlee-lab", + "name": "WalkerLee", + "avatar_url": "https://avatars.githubusercontent.com/u/8426753?v=4", + "profile": "https://github.com/walkerlee-lab", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "fullofjoy", + "name": "jianghang", + "avatar_url": "https://avatars.githubusercontent.com/u/30247571?v=4", + "profile": "https://github.com/fullofjoy", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "ChineseTony", + "name": "ChineseTony", + "avatar_url": "https://avatars.githubusercontent.com/u/24618786?v=4", + "profile": "https://github.com/ChineseTony", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "wyt199905", + "name": "wyt199905", + "avatar_url": "https://avatars.githubusercontent.com/u/85098809?v=4", + "profile": "https://github.com/wyt199905", + "contributions": [ + "code" + ] + }, + { + "login": "weifuqing", + "name": "卫傅庆", + "avatar_url": "https://avatars.githubusercontent.com/u/13931013?v=4", + "profile": "https://github.com/weifuqing", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "zklmcookle", + "name": "zklmcookle", + "avatar_url": "https://avatars.githubusercontent.com/u/107192352?v=4", + "profile": "https://github.com/zklmcookle", + "contributions": [ + "code" + ] + }, + { + "login": "DevilX5", + "name": "DevilX5", + "avatar_url": "https://avatars.githubusercontent.com/u/13269921?v=4", + "profile": "https://github.com/DevilX5", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "djzeng", + "name": "tea", + "avatar_url": "https://avatars.githubusercontent.com/u/14074864?v=4", + "profile": "https://github.com/djzeng", + "contributions": [ + "code" + ] + }, + { + "login": "yangshihui", + "name": "yangshihui", + "avatar_url": "https://avatars.githubusercontent.com/u/28550208?v=4", + "profile": "https://github.com/yangshihui", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "DreamGirl524", + "name": "DreamGirl524", + "avatar_url": "https://avatars.githubusercontent.com/u/81132838?v=4", + "profile": "https://github.com/DreamGirl524", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "gzwlly", + "name": "gzwlly", + "avatar_url": "https://avatars.githubusercontent.com/u/83171907?v=4", + "profile": "https://github.com/gzwlly", + "contributions": [ + "doc" + ] + }, + { + "login": "cuipiheqiuqiu", + "name": "cuipiheqiuqiu", + "avatar_url": "https://avatars.githubusercontent.com/u/76642201?v=4", + "profile": "https://github.com/cuipiheqiuqiu", + "contributions": [ + "code", + "test", + "design" + ] + }, + { + "login": "oyiyou", + "name": "lambert", + "avatar_url": "https://avatars.githubusercontent.com/u/39228891?v=4", + "profile": "https://github.com/oyiyou", + "contributions": [ + "code" + ] + }, + { + "login": "mroldx", + "name": "mroldx", + "avatar_url": "https://avatars.githubusercontent.com/u/34847828?v=4", + "profile": "http://mroldx.xyz/", + "contributions": [ + "doc" + ] + }, + { + "login": "woshiniusange", + "name": "woshiniusange", + "avatar_url": "https://avatars.githubusercontent.com/u/91513022?v=4", + "profile": "https://github.com/woshiniusange", + "contributions": [ + "doc" + ] + }, + { + "login": "VampireAchao", + "name": "VampireAchao", + "avatar_url": "https://avatars.githubusercontent.com/u/52746628?v=4", + "profile": "https://vampireachao.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "Ceilzcx", + "name": "zcx", + "avatar_url": "https://avatars.githubusercontent.com/u/48920254?v=4", + "profile": "https://github.com/Ceilzcx", + "contributions": [ + "code", + "bug", + "design", + "test", + "blog" + ] + }, + { + "login": "CharlieXCL", + "name": "CharlieXCL", + "avatar_url": "https://avatars.githubusercontent.com/u/91540487?v=4", + "profile": "https://github.com/CharlieXCL", + "contributions": [ + "doc" + ] + }, + { + "login": "Privauto", + "name": "Privauto", + "avatar_url": "https://avatars.githubusercontent.com/u/36581456?v=4", + "profile": "https://github.com/Privauto", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "emrys-he", + "name": "emrys", + "avatar_url": "https://avatars.githubusercontent.com/u/5848915?v=4", + "profile": "https://github.com/emrys-he", + "contributions": [ + "doc" + ] + }, + { + "login": "SxLiuYu", + "name": "SxLiuYu", + "avatar_url": "https://avatars.githubusercontent.com/u/95198625?v=4", + "profile": "https://github.com/SxLiuYu", + "contributions": [ + "bug" + ] + }, + { + "login": "all-contributors", + "name": "All Contributors", + "avatar_url": "https://avatars.githubusercontent.com/u/46410174?v=4", + "profile": "https://allcontributors.org", + "contributions": [ + "doc" + ] + }, + { + "login": "gxc-myh", + "name": "铁甲小宝", + "avatar_url": "https://avatars.githubusercontent.com/u/85919258?v=4", + "profile": "https://github.com/gxc-myh", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "click33", + "name": "click33", + "avatar_url": "https://avatars.githubusercontent.com/u/36243476?v=4", + "profile": "https://github.com/click33", + "contributions": [ + "doc" + ] + }, + { + "login": "bwcx-jzy", + "name": "蒋小小", + "avatar_url": "https://avatars.githubusercontent.com/u/16408873?v=4", + "profile": "https://jpom.io/", + "contributions": [ + "doc" + ] + }, + { + "login": "kevinhuangwl", + "name": "Kevin Huang", + "avatar_url": "https://avatars.githubusercontent.com/u/28581579?v=4", + "profile": "https://www.zhihu.com/people/kevinbauer", + "contributions": [ + "doc" + ] + }, + { + "login": "TJxiaobao", + "name": "铁甲小宝", + "avatar_url": "https://avatars.githubusercontent.com/u/85919258?v=4", + "profile": "https://github.com/TJxiaobao", + "contributions": [ + "bug", + "code", + "doc", + "test", + "design" + ] + }, + { + "login": "Jack-123-power", + "name": "Captain Jack", + "avatar_url": "https://avatars.githubusercontent.com/u/84333501?v=4", + "profile": "https://github.com/Jack-123-power", + "contributions": [ + "doc" + ] + }, + { + "login": "haibo-duan", + "name": "haibo.duan", + "avatar_url": "https://avatars.githubusercontent.com/u/7974845?v=4", + "profile": "https://github.com/haibo-duan", + "contributions": [ + "test", + "code" + ] + }, + { + "login": "assassinfym", + "name": "assassin", + "avatar_url": "https://avatars.githubusercontent.com/u/15188754?v=4", + "profile": "https://github.com/assassinfym", + "contributions": [ + "bug", + "code" + ] + }, + { + "login": "csyshu", + "name": "Reverse wind", + "avatar_url": "https://avatars.githubusercontent.com/u/46591658?v=4", + "profile": "https://github.com/csyshu", + "contributions": [ + "test", + "code" + ] + }, + { + "login": "luxx-lq", + "name": "luxx", + "avatar_url": "https://avatars.githubusercontent.com/u/58515565?v=4", + "profile": "https://github.com/luxx-lq", + "contributions": [ + "code" + ] + }, + { + "login": "eltociear", + "name": "Ikko Ashimine", + "avatar_url": "https://avatars.githubusercontent.com/u/22633385?v=4", + "profile": "https://bandism.net/", + "contributions": [ + "doc" + ] + }, + { + "login": "zenan08", + "name": "leizenan", + "avatar_url": "https://avatars.githubusercontent.com/u/80514991?v=4", + "profile": "https://github.com/zenan08", + "contributions": [ + "code" + ] + }, + { + "login": "BKing2020", + "name": "BKing", + "avatar_url": "https://avatars.githubusercontent.com/u/28869121?v=4", + "profile": "https://github.com/BKing2020", + "contributions": [ + "doc" + ] + }, + { + "login": "xingshuaiLi", + "name": "xingshuaiLi", + "avatar_url": "https://avatars.githubusercontent.com/u/119487588?v=4", + "profile": "https://github.com/xingshuaiLi", + "contributions": [ + "doc" + ] + }, + { + "login": "wangke6666", + "name": "wangke6666", + "avatar_url": "https://avatars.githubusercontent.com/u/113656595?v=4", + "profile": "https://github.com/wangke6666", + "contributions": [ + "doc" + ] + }, + { + "login": "LWBobo", + "name": "刺猬", + "avatar_url": "https://avatars.githubusercontent.com/u/50368698?v=4", + "profile": "https://github.com/LWBobo", + "contributions": [ + "bug", + "code" + ] + }, + { + "login": "zanglikun", + "name": "Haste", + "avatar_url": "https://avatars.githubusercontent.com/u/61591648?v=4", + "profile": "http://www.zanglikun.com", + "contributions": [ + "code" + ] + }, + { + "login": "SuitSmile", + "name": "zhongshi.yi", + "avatar_url": "https://avatars.githubusercontent.com/u/38679717?v=4", + "profile": "https://github.com/SuitSmile", + "contributions": [ + "doc" + ] + }, + { + "login": "zzzhangqi", + "name": "Qi Zhang", + "avatar_url": "https://avatars.githubusercontent.com/u/39754275?v=4", + "profile": "https://www.smallq.cn", + "contributions": [ + "doc" + ] + }, + { + "login": "MrAndyMing", + "name": "MrAndyMing", + "avatar_url": "https://avatars.githubusercontent.com/u/49541483?v=4", + "profile": "https://github.com/MrAndyMing", + "contributions": [ + "doc" + ] + }, + { + "login": "idongliming", + "name": "idongliming", + "avatar_url": "https://avatars.githubusercontent.com/u/31564353?v=4", + "profile": "https://idongliming.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "earthjasonlin", + "name": "Zichao Lin", + "avatar_url": "https://avatars.githubusercontent.com/u/83632110?v=4", + "profile": "https://earthjasonlin.github.io", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "liudonghua123", + "name": "liudonghua", + "avatar_url": "https://avatars.githubusercontent.com/u/2276718?v=4", + "profile": "http://blog.liudonghua.com", + "contributions": [ + "code", + "ideas" + ] + }, + { + "login": "orangeyts", + "name": "Jerry", + "avatar_url": "https://avatars.githubusercontent.com/u/4250869?v=4", + "profile": "https://github.com/orangeyts", + "contributions": [ + "code", + "test", + "ideas" + ] + }, + { + "login": "yanhom1314", + "name": "yanhom", + "avatar_url": "https://avatars.githubusercontent.com/u/13051908?v=4", + "profile": "https://dynamictp.cn", + "contributions": [ + "doc" + ] + }, + { + "login": "fengshunli", + "name": "fsl", + "avatar_url": "https://avatars.githubusercontent.com/u/18587688?v=4", + "profile": "https://www.jianshu.com/u/a8f822c04f67", + "contributions": [ + "code" + ] + }, + { + "login": "xttttv", + "name": "xttttv", + "avatar_url": "https://avatars.githubusercontent.com/u/116323904?v=4", + "profile": "https://github.com/xttttv", + "contributions": [ + "doc" + ] + }, + { + "login": "NavinKumarBarnwal", + "name": "NavinKumarBarnwal", + "avatar_url": "https://avatars.githubusercontent.com/u/44504274?v=4", + "profile": "https://github.com/NavinKumarBarnwal", + "contributions": [ + "code" + ] + }, + { + "login": "z641205699", + "name": "Zakkary", + "avatar_url": "https://avatars.githubusercontent.com/u/45276423?v=4", + "profile": "https://github.com/z641205699", + "contributions": [ + "doc" + ] + }, + { + "login": "898349230", + "name": "sunxinbo", + "avatar_url": "https://avatars.githubusercontent.com/u/21972532?v=4", + "profile": "https://github.com/898349230", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "ldzbook", + "name": "ldzbook", + "avatar_url": "https://avatars.githubusercontent.com/u/13903790?v=4", + "profile": "https://github.com/ldzbook", + "contributions": [ + "doc", + "bug" + ] + }, + { + "login": "SurryChen", + "name": "余与雨", + "avatar_url": "https://avatars.githubusercontent.com/u/91116490?v=4", + "profile": "https://github.com/SurryChen", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "MysticalDream", + "name": "MysticalDream", + "avatar_url": "https://avatars.githubusercontent.com/u/78899028?v=4", + "profile": "https://github.com/MysticalDream", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "zhouyoulin12", + "name": "zhouyoulin12", + "avatar_url": "https://avatars.githubusercontent.com/u/17086633?v=4", + "profile": "https://github.com/zhouyoulin12", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "jerjjj", + "name": "jerjjj", + "avatar_url": "https://avatars.githubusercontent.com/u/93431283?v=4", + "profile": "https://github.com/jerjjj", + "contributions": [ + "code" + ] + }, + { + "login": "wjl110", + "name": "wjl110", + "avatar_url": "https://avatars.githubusercontent.com/u/53851034?v=4", + "profile": "https://wjl110.xyz/", + "contributions": [ + "code" + ] + }, + { + "login": "ngyhd", + "name": "Sean", + "avatar_url": "https://avatars.githubusercontent.com/u/29095207?v=4", + "profile": "https://github.com/ngyhd", + "contributions": [ + "doc" + ] + }, + { + "login": "Daydreamer-ia", + "name": "chenyiqin", + "avatar_url": "https://avatars.githubusercontent.com/u/83362909?v=4", + "profile": "https://github.com/Daydreamer-ia", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "hudongdong129", + "name": "hudongdong129", + "avatar_url": "https://avatars.githubusercontent.com/u/34374227?v=4", + "profile": "https://github.com/hudongdong129", + "contributions": [ + "code", + "test", + "doc", + "design" + ] + }, + { + "login": "TherChenYang", + "name": "TherChenYang", + "avatar_url": "https://avatars.githubusercontent.com/u/124348939?v=4", + "profile": "https://github.com/TherChenYang", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "HattoriHenzo", + "name": "HattoriHenzo", + "avatar_url": "https://avatars.githubusercontent.com/u/5141285?v=4", + "profile": "https://github.com/HattoriHenzo", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "ycilry", + "name": "ycilry", + "avatar_url": "https://avatars.githubusercontent.com/u/63967101?v=4", + "profile": "https://github.com/ycilry", + "contributions": [ + "doc" + ] + }, + { + "login": "aoshiguchen", + "name": "aoshiguchen", + "avatar_url": "https://avatars.githubusercontent.com/u/10580997?v=4", + "profile": "https://github.com/aoshiguchen", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "caibenxiang", + "name": "蔡本祥", + "avatar_url": "https://avatars.githubusercontent.com/u/4568241?v=4", + "profile": "https://github.com/caibenxiang", + "contributions": [ + "code" + ] + }, + { + "login": "lifefloating", + "name": "浮游", + "avatar_url": "https://avatars.githubusercontent.com/u/13827124?v=4", + "profile": "http://www.fckeverything.cn:4000/", + "contributions": [ + "code" + ] + }, + { + "login": "Grass-Life", + "name": "Grass-Life", + "avatar_url": "https://avatars.githubusercontent.com/u/114381513?v=4", + "profile": "https://github.com/Grass-Life", + "contributions": [ + "code" + ] + }, + { + "login": "xiaohe428", + "name": "xiaohe428", + "avatar_url": "https://avatars.githubusercontent.com/u/99130317?v=4", + "profile": "https://github.com/xiaohe428", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "baiban114", + "name": "TableRow", + "avatar_url": "https://avatars.githubusercontent.com/u/59152619?v=4", + "profile": "https://github.com/baiban114", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "ByteIDance", + "name": "ByteIDance", + "avatar_url": "https://avatars.githubusercontent.com/u/100207562?v=4", + "profile": "https://github.com/ByteIDance", + "contributions": [ + "code" + ] + }, + { + "login": "mangel2002", + "name": "Jangfe", + "avatar_url": "https://avatars.githubusercontent.com/u/9348020?v=4", + "profile": "https://github.com/mangel2002", + "contributions": [ + "code" + ] + }, + { + "login": "zqr10159", + "name": "zqr10159", + "avatar_url": "https://avatars.githubusercontent.com/u/30048352?v=4", + "profile": "https://github.com/zqr10159", + "contributions": [ + "doc", + "code", + "blog", + "bug", + "test", + "design" + ] + }, + { + "login": "vinci-897", + "name": "vinci", + "avatar_url": "https://avatars.githubusercontent.com/u/55838224?v=4", + "profile": "https://github.com/vinci-897", + "contributions": [ + "code", + "doc", + "design" + ] + }, + { + "login": "js110", + "name": "js110", + "avatar_url": "https://avatars.githubusercontent.com/u/51191863?v=4", + "profile": "https://github.com/js110", + "contributions": [ + "code" + ] + }, + { + "login": "JavaLionLi", + "name": "CrazyLionLi", + "avatar_url": "https://avatars.githubusercontent.com/u/31852897?v=4", + "profile": "https://github.com/JavaLionLi", + "contributions": [ + "doc" + ] + }, + { + "login": "banmajio", + "name": "banmajio", + "avatar_url": "https://avatars.githubusercontent.com/u/53471385?v=4", + "profile": "http://www.banmajio.com", + "contributions": [ + "code" + ] + }, + { + "login": "topsuder", + "name": "topsuder", + "avatar_url": "https://avatars.githubusercontent.com/u/69955165?v=4", + "profile": "https://suder.fun", + "contributions": [ + "code" + ] + }, + { + "login": "richar2022", + "name": "richar2022", + "avatar_url": "https://avatars.githubusercontent.com/u/129016397?v=4", + "profile": "https://github.com/richar2022", + "contributions": [ + "code" + ] + }, + { + "login": "fcb-xiaobo", + "name": "fcb-xiaobo", + "avatar_url": "https://avatars.githubusercontent.com/u/60566194?v=4", + "profile": "https://github.com/fcb-xiaobo", + "contributions": [ + "code" + ] + }, + { + "login": "wenkyzhang", + "name": "wenkyzhang", + "avatar_url": "https://avatars.githubusercontent.com/u/13983669?v=4", + "profile": "https://github.com/wenkyzhang", + "contributions": [ + "doc" + ] + }, + { + "login": "ZangJuxy", + "name": "ZangJuxy", + "avatar_url": "https://avatars.githubusercontent.com/u/71380295?v=4", + "profile": "https://github.com/ZangJuxy", + "contributions": [ + "doc" + ] + }, + { + "login": "l646505418", + "name": "l646505418", + "avatar_url": "https://avatars.githubusercontent.com/u/50475131?v=4", + "profile": "https://github.com/l646505418", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "Carpe-Wang", + "name": "Carpe-Wang", + "avatar_url": "https://avatars.githubusercontent.com/u/78642589?v=4", + "profile": "http://www.carpewang.com", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "moshu023", + "name": "莫枢", + "avatar_url": "https://avatars.githubusercontent.com/u/48593205?v=4", + "profile": "https://github.com/moshu023", + "contributions": [ + "code" + ] + }, + { + "login": "huangcanda", + "name": "huangcanda", + "avatar_url": "https://avatars.githubusercontent.com/u/4470566?v=4", + "profile": "https://github.com/huangcanda", + "contributions": [ + "code" + ] + }, + { + "login": "Architect-Java", + "name": "世纪末的架构师", + "avatar_url": "https://avatars.githubusercontent.com/u/85340613?v=4", + "profile": "https://www.zrkizzy.com", + "contributions": [ + "code" + ] + }, + { + "login": "ShuningWan", + "name": "ShuningWan", + "avatar_url": "https://avatars.githubusercontent.com/u/31086770?v=4", + "profile": "https://github.com/ShuningWan", + "contributions": [ + "doc" + ] + }, + { + "login": "MrYZhou", + "name": "MrYZhou", + "avatar_url": "https://avatars.githubusercontent.com/u/44339602?v=4", + "profile": "https://github.com/MrYZhou", + "contributions": [ + "doc" + ] + }, + { + "login": "suncqujsj", + "name": "suncqujsj", + "avatar_url": "https://avatars.githubusercontent.com/u/8012932?v=4", + "profile": "https://github.com/suncqujsj", + "contributions": [ + "doc" + ] + }, + { + "login": "sunqinbo", + "name": "sunqinbo", + "avatar_url": "https://avatars.githubusercontent.com/u/1428540?v=4", + "profile": "https://github.com/sunqinbo", + "contributions": [ + "code" + ] + }, + { + "login": "haoww", + "name": "haoww", + "avatar_url": "https://avatars.githubusercontent.com/u/32739294?v=4", + "profile": "https://github.com/haoww", + "contributions": [ + "doc" + ] + }, + { + "login": "i-mayuan", + "name": "i-mayuan", + "avatar_url": "https://avatars.githubusercontent.com/u/101498477?v=4", + "profile": "https://github.com/i-mayuan", + "contributions": [ + "doc" + ] + }, + { + "login": "fengruge", + "name": "fengruge", + "avatar_url": "https://avatars.githubusercontent.com/u/85803831?v=4", + "profile": "https://github.com/fengruge", + "contributions": [ + "doc" + ] + }, + { + "login": "aystzh", + "name": "zhanghuan", + "avatar_url": "https://avatars.githubusercontent.com/u/38125392?v=4", + "profile": "https://github.com/aystzh", + "contributions": [ + "code" + ] + }, + { + "login": "shenyumin", + "name": "shenymin", + "avatar_url": "https://avatars.githubusercontent.com/u/8438506?v=4", + "profile": "https://github.com/shenyumin", + "contributions": [ + "code" + ] + }, + { + "login": "dhruva1995", + "name": "Dhruva Chandra", + "avatar_url": "https://avatars.githubusercontent.com/u/12976351?v=4", + "profile": "https://github.com/dhruva1995", + "contributions": [ + "code" + ] + }, + { + "login": "weiwang988", + "name": "miss_z", + "avatar_url": "https://avatars.githubusercontent.com/u/58241726?v=4", + "profile": "https://github.com/weiwang988", + "contributions": [ + "doc" + ] + }, + { + "login": "wyt990", + "name": "wyt990", + "avatar_url": "https://avatars.githubusercontent.com/u/86013697?v=4", + "profile": "https://github.com/wyt990", + "contributions": [ + "code" + ] + }, + { + "login": "licocon", + "name": "licocon", + "avatar_url": "https://avatars.githubusercontent.com/u/36863277?v=4", + "profile": "https://github.com/licocon", + "contributions": [ + "code" + ] + }, + { + "login": "2406450951", + "name": "Mi Na", + "avatar_url": "https://avatars.githubusercontent.com/u/48074721?v=4", + "profile": "https://github.com/2406450951", + "contributions": [ + "code" + ] + }, + { + "login": "Kylin-Guo", + "name": "Kylin-Guo", + "avatar_url": "https://avatars.githubusercontent.com/u/131239856?v=4", + "profile": "https://github.com/Kylin-Guo", + "contributions": [ + "doc" + ] + }, + { + "login": "1797899698", + "name": "Mr灬Dong先生", + "avatar_url": "https://avatars.githubusercontent.com/u/40411650?v=4", + "profile": "https://github.com/1797899698", + "contributions": [ + "code" + ] + }, + { + "login": "Neilblaze", + "name": "Pratyay Banerjee", + "avatar_url": "https://avatars.githubusercontent.com/u/48355572?v=4", + "profile": "http://neilblaze.live", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "yujianzhong520", + "name": "yujianzhong520", + "avatar_url": "https://avatars.githubusercontent.com/u/63705063?v=4", + "profile": "https://github.com/yujianzhong520", + "contributions": [ + "code" + ] + }, + { + "login": "sppan24", + "name": "SPPan", + "avatar_url": "https://avatars.githubusercontent.com/u/15795173?v=4", + "profile": "https://sppan24.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "1130600015", + "name": "ZhangJiashu", + "avatar_url": "https://avatars.githubusercontent.com/u/67859663?v=4", + "profile": "https://github.com/1130600015", + "contributions": [ + "code" + ] + }, + { + "login": "QZmp236478", + "name": "impress", + "avatar_url": "https://avatars.githubusercontent.com/u/56623162?v=4", + "profile": "https://github.com/QZmp236478", + "contributions": [ + "code" + ] + }, + { + "login": "jx3775250", + "name": "凌晨一点半", + "avatar_url": "https://avatars.githubusercontent.com/u/40455946?v=4", + "profile": "https://github.com/jx3775250", + "contributions": [ + "doc" + ] + }, + { + "login": "eeshaanSA", + "name": "Eeshaan Sawant", + "avatar_url": "https://avatars.githubusercontent.com/u/100678386?v=4", + "profile": "https://github.com/eeshaanSA", + "contributions": [ + "code" + ] + }, + { + "login": "nandofromthebando", + "name": "nandofromthebando", + "avatar_url": "https://avatars.githubusercontent.com/u/87321214?v=4", + "profile": "https://github.com/nandofromthebando", + "contributions": [ + "code" + ] + }, + { + "login": "caiboking", + "name": "caiboking", + "avatar_url": "https://avatars.githubusercontent.com/u/6509883?v=4", + "profile": "https://github.com/caiboking", + "contributions": [ + "code" + ] + }, + { + "login": "baixing99", + "name": "baixing99", + "avatar_url": "https://avatars.githubusercontent.com/u/73473087?v=4", + "profile": "https://github.com/baixing99", + "contributions": [ + "code" + ] + }, + { + "login": "ifrenzyc", + "name": "Yang Chuang", + "avatar_url": "https://avatars.githubusercontent.com/u/543927?v=4", + "profile": "https://github.com/ifrenzyc", + "contributions": [ + "code" + ] + }, + { + "login": "wlin20", + "name": "wlin20", + "avatar_url": "https://avatars.githubusercontent.com/u/20657577?v=4", + "profile": "https://github.com/wlin20", + "contributions": [ + "code" + ] + }, + { + "login": "guojing1983", + "name": "guojing1983", + "avatar_url": "https://avatars.githubusercontent.com/u/60596094?v=4", + "profile": "https://github.com/guojing1983", + "contributions": [ + "code" + ] + }, + { + "login": "itxxq", + "name": "moxi", + "avatar_url": "https://avatars.githubusercontent.com/u/46962357?v=4", + "profile": "https://github.com/itxxq", + "contributions": [ + "doc" + ] + }, + { + "login": "qq471754603", + "name": "qq471754603", + "avatar_url": "https://avatars.githubusercontent.com/u/23146592?v=4", + "profile": "https://github.com/qq471754603", + "contributions": [ + "code" + ] + }, + { + "login": "san346596324", + "name": "渭雨", + "avatar_url": "https://avatars.githubusercontent.com/u/30828520?v=4", + "profile": "https://github.com/san346596324", + "contributions": [ + "code" + ] + }, + { + "login": "luoxuanzao", + "name": "liuxuezhuo", + "avatar_url": "https://avatars.githubusercontent.com/u/44692579?v=4", + "profile": "https://github.com/luoxuanzao", + "contributions": [ + "code" + ] + }, + { + "login": "lisongning", + "name": "lisongning", + "avatar_url": "https://avatars.githubusercontent.com/u/93140178?v=4", + "profile": "https://github.com/lisongning", + "contributions": [ + "code" + ] + }, + { + "login": "YutingNie", + "name": "YutingNie", + "avatar_url": "https://avatars.githubusercontent.com/u/104416402?v=4", + "profile": "https://github.com/YutingNie", + "contributions": [ + "code", + "doc", + "design" + ] + }, + { + "login": "mikezzb", + "name": "Mike Zhou", + "avatar_url": "https://avatars.githubusercontent.com/u/23418428?v=4", + "profile": "https://github.com/mikezzb", + "contributions": [ + "code", + "doc", + "design" + ] + }, + { + "login": "lynx009", + "name": "lynx009", + "avatar_url": "https://avatars.githubusercontent.com/u/105542329?v=4", + "profile": "https://github.com/lynx009", + "contributions": [ + "doc" + ] + }, + { + "login": "littlezhongzer", + "name": "littlezhongzer", + "avatar_url": "https://avatars.githubusercontent.com/u/33685289?v=4", + "profile": "https://github.com/littlezhongzer", + "contributions": [ + "code" + ] + }, + { + "login": "ChenXiangxxxxx", + "name": "ChenXiangxxxxx", + "avatar_url": "https://avatars.githubusercontent.com/u/90089594?v=4", + "profile": "https://github.com/ChenXiangxxxxx", + "contributions": [ + "code" + ] + }, + { + "login": "Mr-zhou315", + "name": "Mr.zhou", + "avatar_url": "https://avatars.githubusercontent.com/u/10276100?v=4", + "profile": "https://github.com/Mr-zhou315", + "contributions": [ + "code" + ] + }, + { + "login": "XimfengYao", + "name": "姚贤丰", + "avatar_url": "https://avatars.githubusercontent.com/u/17541537?v=4", + "profile": "https://github.com/XimfengYao", + "contributions": [ + "code" + ] + }, + { + "login": "LINGLUOJUN", + "name": "lingluojun", + "avatar_url": "https://avatars.githubusercontent.com/u/16778977?v=4", + "profile": "https://github.com/LINGLUOJUN", + "contributions": [ + "code" + ] + }, + { + "login": "luelueking", + "name": "1ue", + "avatar_url": "https://avatars.githubusercontent.com/u/93204032?v=4", + "profile": "http://www.luelueking.com", + "contributions": [ + "code" + ] + }, + { + "login": "qyaaaa", + "name": "qyaaaa", + "avatar_url": "https://avatars.githubusercontent.com/u/67301054?v=4", + "profile": "http://www.jimmyqiao.top", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "novohit", + "name": "novohit", + "avatar_url": "https://avatars.githubusercontent.com/u/101090395?v=4", + "profile": "https://novohit.top", + "contributions": [ + "code" + ] + }, + { + "login": "rbsrcy", + "name": "zhuoshangyi", + "avatar_url": "https://avatars.githubusercontent.com/u/4798540?v=4", + "profile": "https://github.com/rbsrcy", + "contributions": [ + "code" + ] + }, + { + "login": "ruanliang-hualun", + "name": "ruanliang", + "avatar_url": "https://avatars.githubusercontent.com/u/65543716?v=4", + "profile": "https://github.com/ruanliang-hualun", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "Eden4701", + "name": "Eden4701", + "avatar_url": "https://avatars.githubusercontent.com/u/68422437?v=4", + "profile": "https://github.com/Eden4701", + "contributions": [ + "code", + "doc", + "design" + ] + }, + { + "login": "XiaTian688", + "name": "XiaTian688", + "avatar_url": "https://avatars.githubusercontent.com/u/111830921?v=4", + "profile": "https://github.com/XiaTian688", + "contributions": [ + "doc" + ] + }, + { + "login": "liyin", + "name": "liyinjiang", + "avatar_url": "https://avatars.githubusercontent.com/u/863169?v=4", + "profile": "https://github.com/liyin", + "contributions": [ + "code" + ] + }, + { + "login": "jiashu1024", + "name": "ZhangJiashu", + "avatar_url": "https://avatars.githubusercontent.com/u/67859663?v=4", + "profile": "https://github.com/jiashu1024", + "contributions": [ + "doc" + ] + }, + { + "login": "1036664317", + "name": "moghn", + "avatar_url": "https://avatars.githubusercontent.com/u/7696697?v=4", + "profile": "https://github.com/1036664317", + "contributions": [ + "doc" + ] + }, + { + "login": "xiaoguolong", + "name": "xiaoguolong", + "avatar_url": "https://avatars.githubusercontent.com/u/33684988?v=4", + "profile": "https://github.com/xiaoguolong", + "contributions": [ + "code" + ] + }, + { + "login": "Clownsw", + "name": "Smliexx", + "avatar_url": "https://avatars.githubusercontent.com/u/28394742?v=4", + "profile": "https://github.com/Clownsw", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "Calvin979", + "name": "Calvin", + "avatar_url": "https://avatars.githubusercontent.com/u/131688897?v=4", + "profile": "https://github.com/Calvin979", + "contributions": [ + "doc", + "code", + "design", + "bug", + "test" + ] + }, + { + "login": "bbelide2", + "name": "Bala Sukesh", + "avatar_url": "https://avatars.githubusercontent.com/u/26840796?v=4", + "profile": "https://github.com/bbelide2", + "contributions": [ + "code" + ] + }, + { + "login": "jinyaoMa", + "name": "Jinyao Ma", + "avatar_url": "https://avatars.githubusercontent.com/u/25066570?v=4", + "profile": "https://github.com/jinyaoMa", + "contributions": [ + "code" + ] + }, + { + "login": "LinuxSuRen", + "name": "Rick", + "avatar_url": "https://avatars.githubusercontent.com/u/1450685?v=4", + "profile": "https://linuxsuren.github.io/open-source-best-practice/", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "ZY945", + "name": "东风", + "avatar_url": "https://avatars.githubusercontent.com/u/74083801?v=4", + "profile": "https://github.com/ZY945", + "contributions": [ + "code", + "design", + "doc", + "bug" + ] + }, + { + "login": "prolevel1", + "name": "sonam singh", + "avatar_url": "https://avatars.githubusercontent.com/u/51995525?v=4", + "profile": "https://github.com/prolevel1", + "contributions": [ + "code" + ] + }, + { + "login": "ZhangZixuan1994", + "name": "ZhangZixuan1994", + "avatar_url": "https://avatars.githubusercontent.com/u/20011653?v=4", + "profile": "https://github.com/ZhangZixuan1994", + "contributions": [ + "code" + ] + }, + { + "login": "hurenjie1", + "name": "SHIG", + "avatar_url": "https://avatars.githubusercontent.com/u/40120355?v=4", + "profile": "https://github.com/hurenjie1", + "contributions": [ + "code" + ] + }, + { + "login": "tslj1024", + "name": "泰上老菌", + "avatar_url": "https://avatars.githubusercontent.com/u/155222677?v=4", + "profile": "https://tslj1024.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "ldysdu", + "name": "ldysdu", + "avatar_url": "https://avatars.githubusercontent.com/u/15815338?v=4", + "profile": "https://github.com/ldysdu", + "contributions": [ + "code" + ] + }, + { + "login": "GEM0816g", + "name": "梁同学", + "avatar_url": "https://avatars.githubusercontent.com/u/85116017?v=4", + "profile": "https://github.com/GEM0816g", + "contributions": [ + "code" + ] + }, + { + "login": "avvCode", + "name": "avv", + "avatar_url": "https://avatars.githubusercontent.com/u/113538532?v=4", + "profile": "https://github.com/avvCode", + "contributions": [ + "code" + ] + }, + { + "login": "yqxxgh", + "name": "yqxxgh", + "avatar_url": "https://avatars.githubusercontent.com/u/42080876?v=4", + "profile": "https://github.com/yqxxgh", + "contributions": [ + "doc", + "code", + "bug" + ] + }, + { + "login": "CharlieShi46", + "name": "CharlieShi46", + "avatar_url": "https://avatars.githubusercontent.com/u/149798885?v=4", + "profile": "https://github.com/CharlieShi46", + "contributions": [ + "code" + ] + }, + { + "login": "Nctllnty", + "name": "Nctllnty", + "avatar_url": "https://avatars.githubusercontent.com/u/33241818?v=4", + "profile": "https://github.com/Nctllnty", + "contributions": [ + "code" + ] + }, + { + "login": "Wang-Yonghao", + "name": "Wang-Yonghao", + "avatar_url": "https://avatars.githubusercontent.com/u/48146606?v=4", + "profile": "https://github.com/Wang-Yonghao", + "contributions": [ + "doc" + ] + }, + { + "login": "dudiao", + "name": "读钓", + "avatar_url": "https://avatars.githubusercontent.com/u/38355949?v=4", + "profile": "https://www.yuque.com/dudiao/yy", + "contributions": [ + "code" + ] + }, + { + "login": "starmilkxin", + "name": "Xin", + "avatar_url": "https://avatars.githubusercontent.com/u/55646681?v=4", + "profile": "https://github.com/starmilkxin", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "handy-git", + "name": "handy", + "avatar_url": "https://avatars.githubusercontent.com/u/32837980?v=4", + "profile": "https://github.com/handy-git", + "contributions": [ + "code" + ] + }, + { + "login": "LiuTianyou", + "name": "LiuTianyou", + "avatar_url": "https://avatars.githubusercontent.com/u/30208283?v=4", + "profile": "https://github.com/LiuTianyou", + "contributions": [ + "code", + "doc", + "bug", + "test", + "blog", + "design" + ] + }, + { + "login": "WinterKi1ler", + "name": "WinterKi1ler", + "avatar_url": "https://avatars.githubusercontent.com/u/160592092?v=4", + "profile": "https://github.com/WinterKi1ler", + "contributions": [ + "code" + ] + }, + { + "login": "miki-hmt", + "name": "miki", + "avatar_url": "https://avatars.githubusercontent.com/u/45377370?v=4", + "profile": "http://sharehoo.cn/", + "contributions": [ + "code" + ] + }, + { + "login": "alpha951", + "name": "Keshav Carpenter", + "avatar_url": "https://avatars.githubusercontent.com/u/85513042?v=4", + "profile": "https://codeflex.substack.com/", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "makechoicenow", + "name": "makechoicenow", + "avatar_url": "https://avatars.githubusercontent.com/u/9911918?v=4", + "profile": "https://github.com/makechoicenow", + "contributions": [ + "code" + ] + }, + { + "login": "gjjjj0101", + "name": "Gao Jian", + "avatar_url": "https://avatars.githubusercontent.com/u/71874373?v=4", + "profile": "https://github.com/gjjjj0101", + "contributions": [ + "test", + "code", + "doc", + "design", + "bug" + ] + }, + { + "login": "dukbong", + "name": "Hyeon Sung", + "avatar_url": "https://avatars.githubusercontent.com/u/37864182?v=4", + "profile": "https://jangto.tistory.com/", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "crossoverJie", + "name": "crossoverJie", + "avatar_url": "https://avatars.githubusercontent.com/u/15684156?v=4", + "profile": "https://crossoverjie.top/", + "contributions": [ + "code", + "doc", + "blog", + "test", + "design" + ] + }, + { + "login": "PeixyJ", + "name": "PeixyJ", + "avatar_url": "https://avatars.githubusercontent.com/u/45998593?v=4", + "profile": "https://github.com/PeixyJ", + "contributions": [ + "code" + ] + }, + { + "login": "Hi-Mr-Wind", + "name": "风如歌", + "avatar_url": "https://avatars.githubusercontent.com/u/85803831?v=4", + "profile": "https://github.com/Hi-Mr-Wind", + "contributions": [ + "code" + ] + }, + { + "login": "MananPoojara", + "name": "Manan Pujara", + "avatar_url": "https://avatars.githubusercontent.com/u/104253184?v=4", + "profile": "https://github.com/MananPoojara", + "contributions": [ + "code" + ] + }, + { + "login": "xuziyang", + "name": "xuziyang", + "avatar_url": "https://avatars.githubusercontent.com/u/8465969?v=4", + "profile": "https://github.com/xuziyang", + "contributions": [ + "code", + "doc", + "bug" + ] + }, + { + "login": "lwqzz", + "name": "lwqzz", + "avatar_url": "https://avatars.githubusercontent.com/u/62584513?v=4", + "profile": "https://github.com/lwqzz", + "contributions": [ + "code" + ] + }, + { + "login": "YxYL6125", + "name": "YxYL", + "avatar_url": "https://avatars.githubusercontent.com/u/91076160?v=4", + "profile": "https://github.com/YxYL6125", + "contributions": [ + "code" + ] + }, + { + "login": "tomorrowshipyltm", + "name": "tomorrowshipyltm", + "avatar_url": "https://avatars.githubusercontent.com/u/61336903?v=4", + "profile": "https://github.com/tomorrowshipyltm", + "contributions": [ + "doc" + ] + }, + { + "login": "15613060203", + "name": "栗磊", + "avatar_url": "https://avatars.githubusercontent.com/u/41351615?v=4", + "profile": "https://github.com/15613060203", + "contributions": [ + "code" + ] + }, + { + "login": "Alanxtl", + "name": "Alan", + "avatar_url": "https://avatars.githubusercontent.com/u/25652981?v=4", + "profile": "https://github.com/Alanxtl", + "contributions": [ + "doc" + ] + }, + { + "login": "zhangshenghang", + "name": "Jast", + "avatar_url": "https://avatars.githubusercontent.com/u/29418975?v=4", + "profile": "http://www.hadoop.wiki/", + "contributions": [ + "code", + "ideas", + "doc", + "blog", + "bug", + "test", + "design" + ] + }, + { + "login": "zuobiao-zhou", + "name": "Zhang Yuxuan", + "avatar_url": "https://avatars.githubusercontent.com/u/61108539?v=4", + "profile": "https://github.com/zuobiao-zhou", + "contributions": [ + "code", + "doc", + "bug", + "blog", + "test" + ] + }, + { + "login": "Pzz-2021", + "name": "P.P.", + "avatar_url": "https://avatars.githubusercontent.com/u/118056735?v=4", + "profile": "https://github.com/Pzz-2021", + "contributions": [ + "code" + ] + }, + { + "login": "LLP2333", + "name": "llp2333", + "avatar_url": "https://avatars.githubusercontent.com/u/61670545?v=4", + "profile": "https://github.com/LLP2333", + "contributions": [ + "code" + ] + }, + { + "login": "HeartLinked", + "name": "feiyang li", + "avatar_url": "https://avatars.githubusercontent.com/u/78212101?v=4", + "profile": "https://github.com/HeartLinked", + "contributions": [ + "doc" + ] + }, + { + "login": "Aias00", + "name": "aias00", + "avatar_url": "https://avatars.githubusercontent.com/u/25810623?v=4", + "profile": "https://github.com/Aias00", + "contributions": [ + "code", + "doc", + "bug", + "ideas", + "test" + ] + }, + { + "login": "westboy", + "name": "Jin", + "avatar_url": "https://avatars.githubusercontent.com/u/6385565?v=4", + "profile": "https://github.com/westboy", + "contributions": [ + "doc" + ] + }, + { + "login": "yuluo-yx", + "name": "YuLuo", + "avatar_url": "https://avatars.githubusercontent.com/u/77964041?v=4", + "profile": "https://blog.csdn.net/qq_52397471", + "contributions": [ + "code", + "bug", + "test", + "blog" + ] + }, + { + "login": "Yanshuming1", + "name": "linDong", + "avatar_url": "https://avatars.githubusercontent.com/u/118667222?v=4", + "profile": "https://github.com/Yanshuming1", + "contributions": [ + "code", + "doc", + "bug" + ] + }, + { + "login": "lwjxy", + "name": "lwjxy", + "avatar_url": "https://avatars.githubusercontent.com/u/52726400?v=4", + "profile": "https://github.com/lwjxy", + "contributions": [ + "code" + ] + }, + { + "login": "Thespica", + "name": "John", + "avatar_url": "https://avatars.githubusercontent.com/u/119573640?v=4", + "profile": "https://thespica.github.io/", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "boatrainlsz", + "name": "boatrainlsz", + "avatar_url": "https://avatars.githubusercontent.com/u/18243785?v=4", + "profile": "https://github.com/boatrainlsz", + "contributions": [ + "doc" + ] + }, + { + "login": "JavaProgrammerLB", + "name": "Bill Lau", + "avatar_url": "https://avatars.githubusercontent.com/u/3973419?v=4", + "profile": "https://www.yitianyigexiangfa.com/", + "contributions": [ + "code" + ] + }, + { + "login": "lw-yang", + "name": "lwyang", + "avatar_url": "https://avatars.githubusercontent.com/u/23456873?v=4", + "profile": "https://github.com/lw-yang", + "contributions": [ + "doc" + ] + }, + { + "login": "xfl12345", + "name": "xfl12345", + "avatar_url": "https://avatars.githubusercontent.com/u/17960863?v=4", + "profile": "https://github.com/xfl12345", + "contributions": [ + "doc" + ] + }, + { + "login": "yykaue", + "name": "Limbo", + "avatar_url": "https://avatars.githubusercontent.com/u/22905143?v=4", + "profile": "https://github.com/yykaue", + "contributions": [ + "code" + ] + }, + { + "login": "irenhongyan", + "name": "哈哈哈哈哈哈哈哈哈", + "avatar_url": "https://avatars.githubusercontent.com/u/53438321?v=4", + "profile": "https://github.com/irenhongyan", + "contributions": [ + "code" + ] + }, + { + "login": "ileonli", + "name": "Leon Li", + "avatar_url": "https://avatars.githubusercontent.com/u/45332412?v=4", + "profile": "https://github.com/ileonli", + "contributions": [ + "code" + ] + }, + { + "login": "killme2008", + "name": "dennis zhuang", + "avatar_url": "https://avatars.githubusercontent.com/u/14142?v=4", + "profile": "http://fnil.net/", + "contributions": [ + "code" + ] + }, + { + "login": "kerwin612", + "name": "Kerwin Bryant", + "avatar_url": "https://avatars.githubusercontent.com/u/3371163?v=4", + "profile": "https://github.com/kerwin612", + "contributions": [ + "design", + "code", + "doc", + "bug" + ] + }, + { + "login": "ShineDevelopment", + "name": "daixianglong", + "avatar_url": "https://avatars.githubusercontent.com/u/59306780?v=4", + "profile": "https://github.com/ShineDevelopment", + "contributions": [ + "doc" + ] + }, + { + "login": "mchgood", + "name": "mchgood", + "avatar_url": "https://avatars.githubusercontent.com/u/38482005?v=4", + "profile": "https://github.com/mchgood", + "contributions": [ + "doc" + ] + }, + { + "login": "pwallk", + "name": "kangli", + "avatar_url": "https://avatars.githubusercontent.com/u/69385076?v=4", + "profile": "https://github.com/pwallk", + "contributions": [ + "code", + "doc", + "bug" + ] + }, + { + "login": "cdphantom", + "name": "cdphantom", + "avatar_url": "https://avatars.githubusercontent.com/u/12674795?v=4", + "profile": "https://github.com/cdphantom", + "contributions": [ + "code" + ] + }, + { + "login": "asd108908382", + "name": "jiawei.guo", + "avatar_url": "https://avatars.githubusercontent.com/u/77717999?v=4", + "profile": "https://github.com/asd108908382", + "contributions": [ + "code" + ] + }, + { + "login": "QBH-insist", + "name": "QBH-insist", + "avatar_url": "https://avatars.githubusercontent.com/u/39401478?v=4", + "profile": "https://github.com/QBH-insist", + "contributions": [ + "code" + ] + }, + { + "login": "jiangsh-ui", + "name": "jiangsh", + "avatar_url": "https://avatars.githubusercontent.com/u/86990361?v=4", + "profile": "https://github.com/jiangsh-ui", + "contributions": [ + "code" + ] + }, + { + "login": "keaifafafa", + "name": "Keaifa", + "avatar_url": "https://avatars.githubusercontent.com/u/83876361?v=4", + "profile": "https://github.com/keaifafafa", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "loong95", + "name": "Loong", + "avatar_url": "https://avatars.githubusercontent.com/u/16333958?v=4", + "profile": "https://github.com/loong95", + "contributions": [ + "code" + ] + }, + { + "login": "ceekay47", + "name": "Chandrakant Vankayalapati", + "avatar_url": "https://avatars.githubusercontent.com/u/104664857?v=4", + "profile": "https://github.com/ceekay47", + "contributions": [ + "code" + ] + }, + { + "login": "MRgenial", + "name": "b_mountain", + "avatar_url": "https://avatars.githubusercontent.com/u/49973336?v=4", + "profile": "https://github.com/MRgenial", + "contributions": [ + "code" + ] + }, + { + "login": "TemirlanBasitov", + "name": "TemirlanBasitov", + "avatar_url": "https://avatars.githubusercontent.com/u/57500808?v=4", + "profile": "https://github.com/TemirlanBasitov", + "contributions": [ + "code" + ] + }, + { + "login": "wyfvsfy", + "name": "wyfvsfy", + "avatar_url": "https://avatars.githubusercontent.com/u/11973517?v=4", + "profile": "https://github.com/wyfvsfy", + "contributions": [ + "doc" + ] + }, + { + "login": "sherry-peng2333", + "name": "sherry-peng2333", + "avatar_url": "https://avatars.githubusercontent.com/u/70619577?v=4", + "profile": "https://github.com/sherry-peng2333", + "contributions": [ + "code" + ] + }, + { + "login": "lixiaobaivv", + "name": "Yzzz", + "avatar_url": "https://avatars.githubusercontent.com/u/39290771?v=4", + "profile": "https://github.com/lixiaobaivv", + "contributions": [ + "code" + ] + }, + { + "login": "pruidong", + "name": "puruidong", + "avatar_url": "https://avatars.githubusercontent.com/u/13309008?v=4", + "profile": "http://www.bckf.cn/", + "contributions": [ + "doc" + ] + }, + { + "login": "shinestare", + "name": "shinestare", + "avatar_url": "https://avatars.githubusercontent.com/u/13570619?v=4", + "profile": "https://github.com/shinestare", + "contributions": [ + "code" + ] + }, + { + "login": "po-168", + "name": "po-168", + "avatar_url": "https://avatars.githubusercontent.com/u/185745593?v=4", + "profile": "https://github.com/po-168", + "contributions": [ + "code" + ] + }, + { + "login": "All-The-Best-for", + "name": "wbs99", + "avatar_url": "https://avatars.githubusercontent.com/u/76414672?v=4", + "profile": "https://github.com/All-The-Best-for", + "contributions": [ + "code" + ] + }, + { + "login": "starryCoder", + "name": "starryCoder", + "avatar_url": "https://avatars.githubusercontent.com/u/46510059?v=4", + "profile": "https://github.com/starryCoder", + "contributions": [ + "code" + ] + }, + { + "login": "hasimmollah", + "name": "hasimmollah", + "avatar_url": "https://avatars.githubusercontent.com/u/32538599?v=4", + "profile": "https://github.com/hasimmollah", + "contributions": [ + "code" + ] + }, + { + "login": "ayu-v0", + "name": "Ayu", + "avatar_url": "https://avatars.githubusercontent.com/u/127600988?v=4", + "profile": "https://github.com/ayu-v0", + "contributions": [ + "code" + ] + }, + { + "login": "Rancho-7", + "name": "Nick Guo", + "avatar_url": "https://avatars.githubusercontent.com/u/59016860?v=4", + "profile": "https://github.com/Rancho-7", + "contributions": [ + "doc", + "code", + "bug" + ] + }, + { + "login": "doveLin0818", + "name": "doveLin", + "avatar_url": "https://avatars.githubusercontent.com/u/190927907?v=4", + "profile": "https://github.com/doveLin0818", + "contributions": [ + "code" + ] + }, + { + "login": "yunfan24", + "name": "yunfan24", + "avatar_url": "https://avatars.githubusercontent.com/u/91836599?v=4", + "profile": "https://zzrl.cc/", + "contributions": [ + "code", + "doc", + "bug", + "test", + "blog" + ] + }, + { + "login": "lctking", + "name": "nullwli", + "avatar_url": "https://avatars.githubusercontent.com/u/168249998?v=4", + "profile": "https://github.com/lctking", + "contributions": [ + "code" + ] + }, + { + "login": "simonsigre", + "name": "Simon Sigré", + "avatar_url": "https://avatars.githubusercontent.com/u/14932913?v=4", + "profile": "https://simonsigre.com/", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "ponfee", + "name": "ponfee", + "avatar_url": "https://avatars.githubusercontent.com/u/46117331?v=4", + "profile": "http://www.ponfee.cn/", + "contributions": [ + "code" + ] + }, + { + "login": "Vedant7789", + "name": "Vedant7789", + "avatar_url": "https://avatars.githubusercontent.com/u/147625492?v=4", + "profile": "https://github.com/Vedant7789", + "contributions": [ + "code" + ] + }, + { + "login": "Craaaaazy77", + "name": "Craaaaazy77", + "avatar_url": "https://avatars.githubusercontent.com/u/23025522?v=4", + "profile": "https://github.com/Craaaaazy77", + "contributions": [ + "doc" + ] + }, + { + "login": "Suvrat1629", + "name": "Suvrat1629", + "avatar_url": "https://avatars.githubusercontent.com/u/140749446?v=4", + "profile": "https://github.com/Suvrat1629", + "contributions": [ + "code" + ] + }, + { + "login": "ghyghoo8", + "name": "ghy", + "avatar_url": "https://avatars.githubusercontent.com/u/363129?v=4", + "profile": "http://ghyghoo8.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "helei1030", + "name": "helei1030", + "avatar_url": "https://avatars.githubusercontent.com/u/11839080?v=4", + "profile": "https://github.com/helei1030", + "contributions": [ + "doc" + ] + }, + { + "login": "pjfanning", + "name": "PJ Fanning", + "avatar_url": "https://avatars.githubusercontent.com/u/11783444?v=4", + "profile": "https://medium.com/@pjfanning", + "contributions": [ + "code", + "bug", + "test" + ] + }, + { + "login": "MonsterChenzhuo", + "name": "monster", + "avatar_url": "https://avatars.githubusercontent.com/u/60029759?v=4", + "profile": "https://github.com/MonsterChenzhuo", + "contributions": [ + "code" + ] + }, + { + "login": "MasamiYui", + "name": "Sherlock Yin", + "avatar_url": "https://avatars.githubusercontent.com/u/22274133?v=4", + "profile": "https://github.com/MasamiYui", + "contributions": [ + "code", + "doc", + "bug" + ] + }, + { + "login": "wanhao23", + "name": "wanhao", + "avatar_url": "https://avatars.githubusercontent.com/u/29560961?v=4", + "profile": "https://github.com/wanhao23", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "jonasHanhan", + "name": "jonasHanhan", + "avatar_url": "https://avatars.githubusercontent.com/u/130035609?v=4", + "profile": "https://github.com/jonasHanhan", + "contributions": [ + "code" + ] + }, + { + "login": "NikhilMurugesan", + "name": "NikhilMurugesan", + "avatar_url": "https://avatars.githubusercontent.com/u/49281792?v=4", + "profile": "https://github.com/NikhilMurugesan", + "contributions": [ + "code" + ] + }, + { + "login": "myangle1120", + "name": "myangle1120", + "avatar_url": "https://avatars.githubusercontent.com/u/19237013?v=4", + "profile": "https://github.com/myangle1120", + "contributions": [ + "code" + ] + }, + { + "login": "yasminvo", + "name": "yasminvo", + "avatar_url": "https://avatars.githubusercontent.com/u/107528848?v=4", + "profile": "https://github.com/yasminvo", + "contributions": [ + "code" + ] + }, + { + "login": "notbugggg", + "name": "不关银渐层的事哦", + "avatar_url": "https://avatars.githubusercontent.com/u/147966331?v=4", + "profile": "https://github.com/notbugggg", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "yyahang", + "name": "yyahang", + "avatar_url": "https://avatars.githubusercontent.com/u/90464876?v=4", + "profile": "https://github.com/yyahang", + "contributions": [ + "code" + ] + }, + { + "login": "JuJinPark", + "name": "jujin", + "avatar_url": "https://avatars.githubusercontent.com/u/44892459?v=4", + "profile": "https://github.com/JuJinPark", + "contributions": [ + "code", + "doc", + "ideas", + "blog" + ] + }, + { + "login": "LL-LIN", + "name": "LL-LIN", + "avatar_url": "https://avatars.githubusercontent.com/u/43002118?v=4", + "profile": "https://github.com/LL-LIN", + "contributions": [ + "code", + "bug" + ] + }, + { + "login": "bigcyy", + "name": "Yang Chen", + "avatar_url": "https://avatars.githubusercontent.com/u/73413979?v=4", + "profile": "https://bigcyy.github.io/", + "contributions": [ + "code", + "doc", + "bug" + ] + }, + { + "login": "sarthakeash", + "name": "Sarthak Arora", + "avatar_url": "https://avatars.githubusercontent.com/u/74091160?v=4", + "profile": "https://github.com/sarthakeash", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "PengJingzhao", + "name": "彭镜肇", + "avatar_url": "https://avatars.githubusercontent.com/u/97368949?v=4", + "profile": "https://github.com/PengJingzhao", + "contributions": [ + "code" + ] + }, + { + "login": "gagaradio", + "name": "Walter Jia", + "avatar_url": "https://avatars.githubusercontent.com/u/18532370?v=4", + "profile": "https://github.com/gagaradio", + "contributions": [ + "code" + ] + }, + { + "login": "boyucjz", + "name": "boyucjz", + "avatar_url": "https://avatars.githubusercontent.com/u/18730041?v=4", + "profile": "https://github.com/boyucjz", + "contributions": [ + "code" + ] + }, + { + "login": "Cyanty", + "name": "Cyanty", + "avatar_url": "https://avatars.githubusercontent.com/u/153884653?v=4", + "profile": "https://github.com/Cyanty", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "KevinLLF", + "name": "Jay丿167", + "avatar_url": "https://avatars.githubusercontent.com/u/85452733?v=4", + "profile": "https://github.com/KevinLLF", + "contributions": [ + "code" + ] + }, + { + "login": "Duansg", + "name": "Duansg", + "avatar_url": "https://avatars.githubusercontent.com/u/112607719?v=4", + "profile": "https://github.com/Duansg", + "contributions": [ + "doc" + ] + }, + { + "login": "xiaomizhou2", + "name": "zhangyaxi", + "avatar_url": "https://avatars.githubusercontent.com/u/47807926?v=4", + "profile": "https://github.com/xiaomizhou2", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "RainBondsongyg", + "name": "songyg", + "avatar_url": "https://avatars.githubusercontent.com/u/94501396?v=4", + "profile": "https://github.com/RainBondsongyg", + "contributions": [ + "doc" + ] + }, + { + "login": "lx1229", + "name": "Liuxin", + "avatar_url": "https://avatars.githubusercontent.com/u/44620005?v=4", + "profile": "https://github.com/lx1229", + "contributions": [ + "code" + ] + }, + { + "login": "yy549159265", + "name": "yy549159265", + "avatar_url": "https://avatars.githubusercontent.com/u/40821310?v=4", + "profile": "https://github.com/yy549159265", + "contributions": [ + "code", + "test", + "design" + ] + }, + { + "login": "cto-huhang", + "name": "cto-huhang", + "avatar_url": "https://avatars.githubusercontent.com/u/53338629?v=4", + "profile": "https://github.com/cto-huhang", + "contributions": [ + "doc" + ] + }, + { + "login": "Saramanda9988", + "name": "LunaRain_079", + "avatar_url": "https://avatars.githubusercontent.com/u/176664901?v=4", + "profile": "https://github.com/Saramanda9988", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "delei", + "name": "DeleiGuo", + "avatar_url": "https://avatars.githubusercontent.com/u/17263766?v=4", + "profile": "https://github.com/delei", + "contributions": [ + "doc", + "code", + "test", + "bug" + ] + }, + { + "login": "chingjustwe", + "name": "Rocky, Chi", + "avatar_url": "https://avatars.githubusercontent.com/u/13643747?v=4", + "profile": "https://github.com/chingjustwe", + "contributions": [ + "code" + ] + }, + { + "login": "rowankid", + "name": "Wenqi Luo", + "avatar_url": "https://avatars.githubusercontent.com/u/18652781?v=4", + "profile": "https://github.com/rowankid", + "contributions": [ + "bug" + ] + }, + { + "login": "tuzuy", + "name": "tuzuy", + "avatar_url": "https://avatars.githubusercontent.com/u/95274591?v=4", + "profile": "https://github.com/tuzuy", + "contributions": [ + "code" + ] + }, + { + "login": "carlpinto25", + "name": "carl pinto", + "avatar_url": "https://avatars.githubusercontent.com/u/117299909?v=4", + "profile": "https://github.com/carlpinto25", + "contributions": [ + "code" + ] + }, + { + "login": "cxhello", + "name": "cxhello", + "avatar_url": "https://avatars.githubusercontent.com/u/49056040?v=4", + "profile": "https://cxhello.top/", + "contributions": [ + "doc" + ] + }, + { + "login": "jl15988", + "name": "会功夫的李白", + "avatar_url": "https://avatars.githubusercontent.com/u/70638770?v=4", + "profile": "https://github.com/jl15988", + "contributions": [ + "code" + ] + }, + { + "login": "AlbertYang0801", + "name": "Albert.Yang", + "avatar_url": "https://avatars.githubusercontent.com/u/37127008?v=4", + "profile": "https://blog.aytop.cloud/", + "contributions": [ + "code" + ] + }, + { + "login": "mengnankkkk", + "name": "zhou yong kang", + "avatar_url": "https://avatars.githubusercontent.com/u/150590575?v=4", + "profile": "https://blog.tokenlen.top/", + "contributions": [ + "code" + ] + }, + { + "login": "warrobe", + "name": "warrobe", + "avatar_url": "https://avatars.githubusercontent.com/u/89446159?v=4", + "profile": "https://github.com/warrobe", + "contributions": [ + "code" + ] + }, + { + "login": "Jetiaime", + "name": "TeAmo", + "avatar_url": "https://avatars.githubusercontent.com/u/93769000?v=4", + "profile": "https://github.com/Jetiaime", + "contributions": [ + "code" + ] + }, + { + "login": "pentium100", + "name": "pentium100", + "avatar_url": "https://avatars.githubusercontent.com/u/27917?v=4", + "profile": "https://github.com/pentium100", + "contributions": [ + "code" + ] + }, + { + "login": "dedyks", + "name": "Dedy Kurniawan Santoso", + "avatar_url": "https://avatars.githubusercontent.com/u/23741665?v=4", + "profile": "https://github.com/dedyks", + "contributions": [ + "code" + ] + }, + { + "login": "KOYR", + "name": "KOYR", + "avatar_url": "https://avatars.githubusercontent.com/u/53216619?v=4", + "profile": "https://github.com/KOYR", + "contributions": [ + "code" + ] + }, + { + "login": "Lathika226", + "name": "LathikaBaddam", + "avatar_url": "https://avatars.githubusercontent.com/u/178710568?v=4", + "profile": "https://github.com/Lathika226", + "contributions": [ + "doc" + ] + }, + { + "login": "Sahil-Shadwal", + "name": "Sahil Shadwal", + "avatar_url": "https://avatars.githubusercontent.com/u/119167601?v=4", + "profile": "https://shadwal.space/", + "contributions": [ + "code" + ] + }, + { + "login": "BhanuNidumolu", + "name": "N.Bhanu Prasad", + "avatar_url": "https://avatars.githubusercontent.com/u/180380413?v=4", + "profile": "https://github.com/BhanuNidumolu", + "contributions": [ + "code" + ] + }, + { + "login": "Prakash1185", + "name": "Prakash Kumar", + "avatar_url": "https://avatars.githubusercontent.com/u/183058331?v=4", + "profile": "https://prakashh-portfolio.vercel.app/", + "contributions": [ + "code" + ] + } + ], + "contributorsPerLine": 7, + "commitType": "docs" +} diff --git a/.asf.yaml b/.asf.yaml new file mode 100644 index 0000000..1dbe364 --- /dev/null +++ b/.asf.yaml @@ -0,0 +1,63 @@ +# +# 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. +# + +github: + description: An AI-powered next-generation open source real-time observability system. + homepage: https://hertzbeat.apache.org/ + labels: + - ai + - agent + - llm + - logs + - monitor + - monitoring + - notifications + - alerting + - self-hosted + - prometheus + - zabbix + - grafana + - metrics + - observability + - uptime + - status + - status-page + - server + - linux + - database + enabled_merge_buttons: + squash: true + merge: false + rebase: false + protected_branches: + master: + required_status_checks: + strict: true + contexts: + - check-license-header + required_pull_request_reviews: + dismiss_stale_reviews: true + required_approving_review_count: 1 + # enable GitHub Dependabot to create PRs for security alerts but not for every dependency update + dependabot_alerts: true + dependabot_updates: false +notifications: + commits: notifications@hertzbeat.apache.org + issues: notifications@hertzbeat.apache.org + pullrequests: notifications@hertzbeat.apache.org + jobs: notifications@hertzbeat.apache.org + discussions: dev@hertzbeat.apache.org diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..cf39f6c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,37 @@ +{ + "name": "HertzBeat DevContainer", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/java:1": { + "version": "17", + "installMaven": "true", + "installGradle": "false" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "20" + }, + "ghcr.io/devcontainers/features/git-lfs:1.1.0": {} + }, + "customizations": { + "vscode": { + "settings": {}, + "extensions": [ + "vscjava.vscode-java-pack", + "editorconfig.editorconfig", + "dbaeumer.vscode-eslint", + "stylelint.vscode-stylelint", + "DavidAnson.vscode-markdownlint", + "ms-azuretools.vscode-docker", + "cweijan.vscode-database-client2", + "GitHub.vscode-pull-request-github" + ] + } + }, + "portsAttributes": { + "4200": { + "label": "HertzBeat Web", + "onAutoForward": "notify" + } + }, + "postCreateCommand": "java -version" +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9466300 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,55 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +# The line max length is 120, match checkstyle rules. +max_line_length = 120 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.json] +tab_width = 2 +indent_size = 2 + +[*.{yml,yaml}] +tab_width = 2 +indent_size = 2 + +[*.xml] +ij_xml_attribute_wrap = off +ij_xml_text_wrap = off +ij_xml_keep_blank_lines = 1 + +[pom.xml] +indent_size = 2 + +[*.java] +indent_size = 4 +tab_width = 4 + +[*.js] +indent_size = 2 + +[*.ts] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2805615 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,25 @@ +# +# 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. +# + +/home export-ignore +/hip export-ignore +/template-marketplace export-ignore +/.github export-ignore +/.idea export-ignore +/.devcontainer export-ignore +.gitattributes export-ignore +.gitignore export-ignore diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..b5a31a7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,78 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: 🐛 Bug Report +description: Problems and issues with code of HertzBeat 🤔. +title: "[BUG] " +labels: ["bug"] +body: +- type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true +- type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: false +- type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: false +- type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + validations: + required: false +- type: textarea + attributes: + label: Environment + description: | + examples: + - **HertzBeat version(s)**: v1.1.1 + value: "HertzBeat version(s):" + render: markdown + validations: + required: false +- type: textarea + attributes: + label: Debug logs + description: | + Add your debug logs here. + validations: + required: false +- type: textarea + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8d9fbbc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,22 @@ +# 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. + +blank_issues_enabled: false +contact_links: + - name: Community Support + url: https://github.com/apache/hertzbeat/discussions + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..243bb92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,57 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: 🚀 Feature Request +description: I have a suggestion (and may want to implement it 🙂)! +title: "[Feature] <title>" +labels: ["new feature"] +body: +- type: textarea + attributes: + label: Feature Request + description: > + Tip: Have you checked the docs https://hertzbeat.apache.org/docs/, GitHub issues whether someone else has already reported your issue? Maybe the feature already exists? + placeholder: > + A concise description of what you're experiencing. + validations: + required: false +- type: textarea + attributes: + label: Is your feature request related to a problem? Please describe + description: A clear and concise description of what the problem is. Ex. I have an issue when [...] + validations: + required: false +- type: textarea + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. Add any considered drawbacks. + validations: + required: false +- type: textarea + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false +- type: textarea + attributes: + label: Additional context + validations: + required: false +- type: markdown + attributes: + value: "Please read the [Contribution Guideline](https://hertzbeat.apache.org/docs/community/contribution) before submitting the PR" diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..36daafa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,35 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: 🤔 Question +description: Usage question that isn't answered in docs or discussion +title: "[Question] <title>" +labels: ["question"] +body: +- type: textarea + attributes: + label: Question + placeholder: > + your question here; + validations: + required: true +- type: markdown + attributes: + value: | + Please pay attention on issues you submitted, because we maybe need more details. + If no response anymore and we cannot reproduce it on current information, we will **close it**. + diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 0000000..4e46853 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,42 @@ +# 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: 🚀 Task +description: Used to create tasks for the community. +title: "[Task] <title>" +labels: ["discussion"] +body: +- type: textarea + attributes: + label: Description + placeholder: A clear and concise description of Task + validations: + required: true +- type: textarea + attributes: + label: Task List + placeholder: | + -[ ] Task1... + -[ ] Task2... + -[ ] Task3... + -[ ] Task4... + validations: + required: false +- type: markdown + attributes: + value: | + Please read the [Contribution Guideline](https://hertzbeat.apache.org/docs/community/contribution) before submitting the PR diff --git a/.github/ISSUE_TEMPLATE/volunteer.yml b/.github/ISSUE_TEMPLATE/volunteer.yml new file mode 100644 index 0000000..2729f39 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/volunteer.yml @@ -0,0 +1,34 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: 🤔 Volunteer +description: Some volunteer help +title: "[Volunteer] <title>" +labels: ["status: volunteer wanted"] +body: +- type: textarea + attributes: + label: Volunteer help + placeholder: > + your need volunteer help here; + validations: + required: true +- type: markdown + attributes: + value: | + Please read the [Contribution Guideline](https://hertzbeat.apache.org/docs/community/contribution) before submitting the PR + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1d2fd3b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +## What's changed? + +<!-- Describe Your PR Here --> + + +## Checklist + +- [ ] I have read the [Contributing Guide](https://hertzbeat.apache.org/docs/community/code_style_and_quality_guide) +- [ ] I have written the necessary doc or comment. +- [ ] I have added the necessary unit tests and all cases have passed. + +## Add or update API + +- [ ] I have added the necessary [e2e tests](https://github.com/apache/hertzbeat/tree/master/e2e) and all cases have passed. diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..9c2f45c --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,115 @@ +# 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. +# + +infra: + - changed-files: + - any-glob-to-any-file: + - ".github/**/*" + - ".asf.yaml" + - ".gitignore" + - ".licenserc.yaml" + - "SECURITY.md" + - "CODE_OF_CONDUCT.md" + - "CONTRIBUTING.md" + +license: + - changed-files: + - any-glob-to-any-file: + - "material/licenses/**/*" + - "LICENSE" + - "NOTICE" + +script: + - changed-files: + - any-glob-to-any-file: + - "script/**/*" + +helm: + - changed-files: + - any-glob-to-any-file: + - "script/helm/**/*" + +kubernetes: + - changed-files: + - any-glob-to-any-file: + - "script/helm/**/*" + +build: + - changed-files: + - any-glob-to-any-file: + - "script/assembly/**/*" + +docker: + - changed-files: + - any-glob-to-any-file: + - "script/docker/**/*" + - "script/docker-compose/**/*" + +docker-compose: + - changed-files: + - any-glob-to-any-file: + - "script/docker-compose/**/*" + +doc: + - changed-files: + - any-glob-to-any-file: + - "**/README.md" + - "**/README_CN.md" + - "home/**/*" + +home: + - changed-files: + - any-glob-to-any-file: + - "home/src/**/*" + +webapp: + - changed-files: + - any-glob-to-any-file: + - "web-app/**/*" + +e2e: + - changed-files: + - any-glob-to-any-file: + - "e2e/**/*" + + +backend: + - changed-files: + - any-glob-to-any-file: + - "hertzbeat-alerter/**/*" + - "hertzbeat-collector/**/*" + - "hertzbeat-common/**/*" + - "hertzbeat-grafana/**/*" + - "hertzbeat-manager/**/*" + - "hertzbeat-push/**/*" + - "hertzbeat-remoting/**/*" + - "hertzbeat-warehouse/**/*" + +collector: + - changed-files: + - any-glob-to-any-file: + - "hertzbeat-collector/**/*" + +monitoring-template: + - changed-files: + - any-glob-to-any-file: + - "hertzbeat-manager/src/main/resources/define/**/*" + +HIP: + - changed-files: + - any-glob-to-any-file: + - 'hip/**' diff --git a/.github/workflows/backend-build-test.yml b/.github/workflows/backend-build-test.yml new file mode 100644 index 0000000..abb52e8 --- /dev/null +++ b/.github/workflows/backend-build-test.yml @@ -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. + +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Backend CI + +on: + push: + branches: [ master, dev, action* ] + paths-ignore: + - '**.md' + - 'home/**' + - 'web-app/**' + - 'script/**' + - 'material/**' + pull_request: + branches: [ master, dev ] + paths-ignore: + - '**.md' + - 'home/**' + - 'web-app/**' + - 'script/**' + - 'material/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + backend-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./script/ci/github-actions/setup-deps + + - name: Build with Maven + run: mvnd clean -B package -pl '!hertzbeat-e2e' -Prelease -Dmaven.test.skip=false --file pom.xml + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + verbose: true + + - name: Upload backend distribution + uses: actions/upload-artifact@v4 + with: + name: backend-dist-${{ github.run_id }} + path: dist/ + retention-days: 1 + compression-level: 0 + + backend-maven-e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./script/ci/github-actions/setup-deps + + - name: Build backend Maven E2E modules + run: | + mvnd clean -B package \ + -pl hertzbeat-e2e/hertzbeat-collector-common-e2e,hertzbeat-e2e/hertzbeat-collector-kafka-e2e,hertzbeat-e2e/hertzbeat-collector-basic-e2e,hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e,hertzbeat-e2e/hertzbeat-log-e2e \ + -am \ + -Dmaven.test.skip=false \ + --file pom.xml + + - name: Upload Maven E2E coverage reports to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + verbose: true + + backend-image-e2e: + needs: backend-build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download backend distribution + uses: actions/download-artifact@v4 + with: + name: backend-dist-${{ github.run_id }} + path: dist/ + + - name: Build Image + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: ./dist + file: ./script/docker/server/Dockerfile + push: false + tags: apache/hertzbeat:test + + - name: Run E2E + run: | + cd e2e + docker compose version + docker compose up --exit-code-from testing --remove-orphans + + # upload application logs + - name: Upload logs & API test reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: hz-logs-${{ github.run_id }} + path: | + e2e/logs/ + e2e/report/ diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..96361ee --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,81 @@ +# 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. + +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + schedule: + - cron: '21 13 * * 4' + push: + branches: [ action* ] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + language: [ 'java', 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Auto build attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Auto build fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/collector-native-build.yml b/.github/workflows/collector-native-build.yml new file mode 100644 index 0000000..582b802 --- /dev/null +++ b/.github/workflows/collector-native-build.yml @@ -0,0 +1,89 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: Collector Native Release + +run-name: Native collector release build (${{ github.ref_name }}) + +on: + workflow_dispatch: + +jobs: + build-native-collector: + name: Native collector (${{ matrix.platform }}) + permissions: + contents: read + timeout-minutes: 120 + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - platform: linux-amd64 + runner: ubuntu-24.04 + archive_ext: tar.gz + - platform: linux-arm64 + runner: ubuntu-24.04-arm + archive_ext: tar.gz + - platform: macos-amd64 + runner: macos-13 + archive_ext: tar.gz + - platform: macos-arm64 + runner: macos-14 + archive_ext: tar.gz + - platform: windows-amd64 + runner: windows-latest + archive_ext: zip + + steps: + - uses: actions/checkout@v4 + + - name: Set up GraalVM JDK 25 + uses: graalvm/setup-graalvm@186d0493a2df5eb62df5ecc498883d18fd58c303 # v1.6.2 + with: + distribution: graalvm-community + java-version: '25' + github-token: ${{ secrets.GITHUB_TOKEN }} + cache: maven + native-image-job-reports: 'true' + + - name: Verify toolchain + shell: pwsh + run: | + java -version + native-image --version + mvn -version + + - name: Build native collector package + run: mvn -B -pl hertzbeat-collector/hertzbeat-collector-collector -am -Pnative -DskipTests package + + - name: Locate native collector package + id: package + shell: pwsh + run: | + $package = Get-ChildItem -Path "dist/apache-hertzbeat-collector-native-*-${{ matrix.platform }}-bin.${{ matrix.archive_ext }}" | Select-Object -First 1 + if (-not $package) { + throw "Native collector package not found for ${{ matrix.platform }}" + } + "archive=$($package.FullName)" >> $env:GITHUB_OUTPUT + + - name: Upload native collector package + uses: actions/upload-artifact@v4 + with: + name: apache-hertzbeat-collector-native-${{ matrix.platform }} + path: ${{ steps.package.outputs.archive }} + retention-days: 14 diff --git a/.github/workflows/doc-build-test.yml b/.github/workflows/doc-build-test.yml new file mode 100644 index 0000000..d69770f --- /dev/null +++ b/.github/workflows/doc-build-test.yml @@ -0,0 +1,80 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: DOC CI + +on: + push: + branches: [ master, dev, action* ] + paths: + - 'home/**' + pull_request: + branches: [ master, dev ] + paths: + - 'home/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + docs-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: pnpm + cache-dependency-path: home/pnpm-lock.yaml + - uses: actions/setup-python@v4 + with: + python-version: '3.8' + + - name: Check Markdown + uses: DavidAnson/markdownlint-cli2-action@8de2aa07cae85fd17c0b35642db70cf5495f1d25 # v24.0.0 + with: + globs: | + ./home/README.md + ./home/blog/**/*.md + ./home/docs/**/*.md + ./home/i18n/zh-cn/docusaurus-plugin-content-blog/**/*.md + ./home/i18n/zh-cn/docusaurus-plugin-content-docs/current/**/*.md + + - name: Check filenames + run: python ./script/ci/docs/check_file_name.py ./script/ci/docs/check_file_name.json + + - name: Dead Link Check + run: | + sudo npm install -g markdown-link-check@3.8.7 + find ./home/docs ./home/blog ./home/i18n/zh-cn/docusaurus-plugin-content-docs/current ./home/i18n/zh-cn/docusaurus-plugin-content-blog -name "*.md" > all_md_files.txt + printf '%s\n' ./home/README.md >> all_md_files.txt + grep -vFf ./script/ci/exclude_files.txt all_md_files.txt > to_check.txt + xargs -P 8 -a to_check.txt -I{} markdown-link-check -c ./script/ci/link_check.json -q "{}" + + - name: PNPM INSTALL + working-directory: home + run: pnpm install + + - name: PNPM BUILD + working-directory: home + run: pnpm run build diff --git a/.github/workflows/doc-deploy.yml b/.github/workflows/doc-deploy.yml new file mode 100644 index 0000000..2c962af --- /dev/null +++ b/.github/workflows/doc-deploy.yml @@ -0,0 +1,94 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: DOC Deploy + +on: + # Trigger on push to master branch when home directory files are changed + push: + branches: [ master ] + paths: + - 'home/**' + + # Allow manual workflow trigger + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Set permissions for deployment +permissions: + contents: write + +jobs: + deploy: + name: Deploy Documentation to asf-site Branch + runs-on: ubuntu-latest + + steps: + # Checkout the repository code + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Setup Node.js environment + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + 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 + run: pnpm install --frozen-lockfile + + # Build the documentation site + - name: Build Documentation + working-directory: home + run: pnpm run build + env: + NODE_OPTIONS: --max-old-space-size=4096 + + # Deploy to asf-site branch using peaceiris/actions-gh-pages + - name: Deploy to asf-site Branch + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./home/build + publish_branch: asf-site + force_orphan: true + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' + commit_message: 'Deploy documentation to asf-site branch' + + # Output deployment status + - name: Deployment Summary + run: | + echo "Documentation deployed successfully!" + echo "Deployment branch: asf-site" + echo "Source branch: ${{ github.ref_name }}" + echo "Commit SHA: ${{ github.sha }}" diff --git a/.github/workflows/doc-pdf-builder.yml b/.github/workflows/doc-pdf-builder.yml new file mode 100644 index 0000000..762fc9b --- /dev/null +++ b/.github/workflows/doc-pdf-builder.yml @@ -0,0 +1,56 @@ +# 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: 'HertzBeat Doc Pdf Builder' +on: + push: + branches: [ page_build* ] + +jobs: + build: + runs-on: macos-latest + steps: + - name: Install Prince + run: | + LANG="zh_CN.UTF-8" + export LANG="zh_CN.UTF-8" + curl https://www.princexml.com/download/prince-15.1-macos.zip -O + tar zxf prince-15.1-macos.zip + cd prince-15.1-macos + yes "" | sudo ./install.sh + + - name: Build CN PDF + run: npx docusaurus-prince-pdf -u https://hertzbeat.apache.org/zh-cn/docs --output docs-cn.pdf + + - name: Build EN PDF + run: npx docusaurus-prince-pdf -u https://hertzbeat.apache.org/docs --output docs-en.pdf + + - name: Upload results + uses: actions/upload-artifact@v4 + with: + name: docs-cn-pdf + path: docs-cn.pdf + if-no-files-found: error + retention-days: 1 + + - name: Upload results + uses: actions/upload-artifact@v4 + with: + name: docs-en-pdf + path: docs-en.pdf + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/frontend-build-test.yml b/.github/workflows/frontend-build-test.yml new file mode 100644 index 0000000..9a403df --- /dev/null +++ b/.github/workflows/frontend-build-test.yml @@ -0,0 +1,55 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Frontend CI + +on: + push: + branches: [ master, dev, action* ] + paths: + - 'web-app/**' + pull_request: + branches: [ master, dev ] + paths: + - 'web-app/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: pnpm + cache-dependency-path: web-app/pnpm-lock.yaml + - name: Install + working-directory: web-app + run: pnpm install + - name: EsLint Test + working-directory: web-app + run: pnpm lint:ts diff --git a/.github/workflows/license-checker.yml b/.github/workflows/license-checker.yml new file mode 100644 index 0000000..44ab112 --- /dev/null +++ b/.github/workflows/license-checker.yml @@ -0,0 +1,43 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: License Checker + +on: + push: + branches: [ master, dev, action* ] + pull_request: + branches: [ master, dev ] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + check-license-header: + name: check-license-header + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Check license header + uses: apache/skywalking-eyes@v0.6.0 diff --git a/.github/workflows/mcp-bashserver-test.yml b/.github/workflows/mcp-bashserver-test.yml new file mode 100644 index 0000000..e18bada --- /dev/null +++ b/.github/workflows/mcp-bashserver-test.yml @@ -0,0 +1,97 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: MCP Bash Server CI + +on: + push: + paths: + - 'mcp-servers/mcp-bash-server/**' + - '.github/workflows/mcp-bashserver-test.yml' + pull_request: + paths: + - 'mcp-servers/mcp-bash-server/**' + - '.github/workflows/mcp-bashserver-test.yml' + +env: + CARGO_TERM_COLOR: always + RUST_VERSION: 1.88.0 + +jobs: + test: + name: Test MCP Bash Server + runs-on: ubuntu-latest + strategy: + matrix: + profile: [dev, release] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: ${{ env.RUST_VERSION }} + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-registry- + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target/ + key: ${{ runner.os }}-cargo-build-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-build-${{ matrix.profile }}- + ${{ runner.os }}-cargo-build- + + - name: Check code format + run: cargo fmt --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml --check + + - name: Run cargo check + run: | + if [ "${{ matrix.profile }}" = "release" ]; then + cargo check --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml + else + cargo check --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml + fi + + - name: Run clippy + run: | + if [ "${{ matrix.profile }}" = "release" ]; then + cargo clippy --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml -- -D warnings + else + cargo clippy --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml -- -D warnings + fi + + - name: Run tests + run: | + if [ "${{ matrix.profile }}" = "release" ]; then + cargo test --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml + else + cargo test --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml + fi diff --git a/.github/workflows/monitor-e2e-test.yml b/.github/workflows/monitor-e2e-test.yml new file mode 100644 index 0000000..3120292 --- /dev/null +++ b/.github/workflows/monitor-e2e-test.yml @@ -0,0 +1,56 @@ +# 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. + +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Monitor E2E Test + + +on: + schedule: + # trigger at 01:00 everyday + - cron: '0 1 * * *' + push: + branches: [ action* ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-license-header: + name: Monitor E2E Test + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: ./script/ci/github-actions/setup-deps + - name: Build with Maven + run: mvnd -B clean package -Prelease -Dmaven.test.skip=true --file pom.xml + - name: Build Image + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: ./dist + file: ./script/docker/server/Dockerfile + push: false + tags: apache/hertzbeat:test + - name: Run K8s Monitor E2E Test + run: | + cd e2e/k8s + docker compose version + docker compose up --exit-code-from testing --remove-orphans diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml new file mode 100644 index 0000000..453649e --- /dev/null +++ b/.github/workflows/nightly-build.yml @@ -0,0 +1,81 @@ +# 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: Nightly CI + +on: + schedule: + # trigger at 00:00 everyday + - cron: '0 0 * * *' + push: + branches: [ action* ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + if: ${{ github.repository == 'apache/hertzbeat' }} + steps: + - uses: actions/checkout@v4 + - uses: ./script/ci/github-actions/setup-deps + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: pnpm + cache-dependency-path: web-app/pnpm-lock.yaml + + - name: Build the Frontend + run: | + cd web-app + pnpm install + pnpm package + + - name: Build the Backend + run: | + mvnd -B clean package -Prelease,Pcluster -Dmaven.test.skip=false --file pom.xml + + - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + - name: Log in to Docker Hub + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and Push Server + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: ./dist + file: ./script/docker/server/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: apache/hertzbeat:nightly + + - name: Build and Push Collector + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: ./dist + file: ./script/docker/collector/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: apache/hertzbeat-collector:nightly diff --git a/.github/workflows/pull-request-robot.yml b/.github/workflows/pull-request-robot.yml new file mode 100644 index 0000000..0179adf --- /dev/null +++ b/.github/workflows/pull-request-robot.yml @@ -0,0 +1,34 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +name: "Pull Request Robot" +on: + - pull_request_target + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Label pull request + uses: actions/labeler@v5 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/labeler.yml + sync-labels: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c66730 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +Thumbs.db +.DS_Store +.gradle +build/ +out/ +micronaut-cli.yml +*.log +package-lock.json +*.zip +jdk/ +data/ + +!/e2e/**/data/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### For icon.png ### +!.idea/icon.png + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +### VS Code ### +.vscode/ + +# dependencies +node_modules +.docusaurus + +# h2 data +*.mv.db +*.trace.db + +# debug env +application-dev.yml +application-mysql.yml +application-pg.yml +/package.json +/yarn.lock diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a7ef9fc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,21 @@ +# +# 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. +# + +[submodule "script/helm/hertzbeat-helm-chart"] + path = script/helm/hertzbeat-helm-chart + url = git@github.com:apache/hertzbeat-helm-chart.git + branch = main diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile new file mode 100644 index 0000000..4c8f464 --- /dev/null +++ b/.gitpod.Dockerfile @@ -0,0 +1,7 @@ +FROM gitpod/workspace-full + +USER gitpod + +RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && \ + sdk install java 21.0.2-tem && \ + sdk default java 21.0.2-tem" diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..7dbc5c1 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,54 @@ +image: + file: .gitpod.Dockerfile + +tasks: + + - name: Run backend + before: cd hertzbeat-startup + command: | + gp sync-await setup-backend + mvn spring-boot:run + + - name: Run frontend + before: cd web-app + command: | + gp sync-await setup-frontend + pnpm start --public-host "`gp url 4200`" + openMode: split-right + + - name: Setup backend + init: | + mvn clean install -DskipTests + command: | + gp sync-done setup-backend + exit 0 + + - name: Setup frontend + init: | + cd web-app && pnpm install + command: | + gp sync-done setup-frontend + exit 0 + openMode: split-right + +vscode: + extensions: + - vscjava.vscode-java-pack + - editorconfig.editorconfig + - dbaeumer.vscode-eslint + - stylelint.vscode-stylelint + - DavidAnson.vscode-markdownlint + - ms-azuretools.vscode-docker + - cweijan.vscode-database-client2 + - GitHub.vscode-pull-request-github + +ports: + - port: 4200 + name: HertzBeat + onOpen: open-browser + + - port: 1157 + onOpen: ignore + + - port: 1158 + onOpen: ignore diff --git a/.licenserc.yaml b/.licenserc.yaml new file mode 100644 index 0000000..a0e6e7f --- /dev/null +++ b/.licenserc.yaml @@ -0,0 +1,108 @@ +# +# 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. +# + +header: + license: + spdx-id: Apache-2.0 + copyright-owner: Apache Software Foundation + + paths-ignore: + - '**/*.md' + - '**/*.json' + - '**/*.iml' + - '**/*.ini' + - '**/*.svg' + - '**/*.png' + - '**/*.MD' + - '**/*.ftl' + - '**/*.tpl' + - '**/*.pl' + - '**/*.dict' + - '**/*.awk' + - "**/*.lock" + - '**/*.crt' + - '**/*.pem' + - '**/*.js' + - '**/*.less' + - '**/*.txt' + - '**/*.st' + - '**/target/**' + - '.gitattributes' + - '**/.gitignore' + - '**/.gitkeep' + - 'home/**' + - 'material/**' + - 'LICENSE' + - 'NOTICE' + - 'DISCLAIMER' + - '**/LICENSE' + - '**/NOTICE' + - '.all-contributorsrc' + - '**/*.AbstractCollect' + - '**/*.Plugin' + - '**/*.MockMaker' + - '.prettierrc' + - '.browserslistrc' + - '.editorconfig' + - '.eslintignore' + - '.eslintrc.js' + - '.stylelintrc' + - '.prettierignore' + - '.prettierrc.js' + - '.gitpod.Dockerfile' + - '.gitpod.yml' + - 'karma.conf.js' + - 'proxy.conf.js' + - 'pnpm-lock.yaml' + - '.helmignore' + - 'script/helm/**' + - 'web-app/src/app/core/**' + - 'web-app/src/app/layout/**' + - 'web-app/src/app/routes/exception/**' + - 'web-app/src/app/routes/routes.module.ts' + - 'web-app/src/app/routes/routes-routing.module.ts' + - 'web-app/src/app/shared/json-schema/**' + - 'web-app/src/app/shared/index.ts' + - 'web-app/src/app/shared/shared.module.ts' + - 'web-app/src/app/shared/shared-delon.module.ts' + - 'web-app/src/app/shared/shared-zorro.module.ts' + - 'web-app/src/app/app.module.ts' + - 'web-app/src/app/app.component.ts' + - 'web-app/src/app/global-config.module.ts' + - 'web-app/src/app/icons-provider.module.ts' + - 'web-app/src/assets/color.less' + - 'web-app/src/assets/style.compact.less' + - 'web-app/src/assets/style.dark.less' + - 'web-app/src/environments/**' + - 'web-app/src/styles/**' + - 'web-app/src/index.html' + - 'web-app/src/main.ts' + - 'web-app/src/polyfills.ts' + - 'web-app/src/style-icons.ts' + - 'web-app/src/style-icons-auto.ts' + - 'web-app/src/style.less' + - 'web-app/src/test.ts' + - 'web-app/src/typings.d.ts' + + comment: on-failure + +dependency: + files: + - pom.xml + - web-app/package.json + + diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..c7e562e --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,55 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +{ + "globs": [ + "home/README.md", + "home/blog/**/*.md", + "home/docs/**/*.md", + "home/i18n/zh-cn/docusaurus-plugin-content-blog/**/*.md", + "home/i18n/zh-cn/docusaurus-plugin-content-docs/current/**/*.md" + ], + "config": { + "default": true, + "MD001": true, + "MD003": true, + "MD013": { + "line_length": 600, + "code_blocks": false, + "tables": false + }, + "MD024": { + "siblings_only": true + }, + "MD025": true, + "MD029": true, + "MD033": { + "allowed_elements": ["table", "tr", "td", "a", "img", "sub", "b", "br", "img", "tbody", "mark", "font", "Tabs", "TabItem"] + }, + "MD036": false, + "MD040": true, + "MD045": true, + "MD046": true, + "MD047": true, + "MD052": true, + "MD060": false + }, + "ignores": [ + "**/node_modules/**", + "**/target/**", + "**/dist/**" + ] +} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..094f182 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,17 @@ +# 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. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6fa9a12 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues and other contributions +that are not aligned to this Code of Conduct or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing or otherwise unacceptable behavior may be +reported by contacting the project team at dev@hertzbeat.apache.org. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate for the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html> + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +<https://www.contributor-covenant.org/faq> diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2f9d6c9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,274 @@ +## Contributor Guide [中文贡献者指南](#贡献者指南) + +> We are committed to maintaining a happy community that helps each other, welcome every contributor to join us! + +### Kinds of Contributions + +> In the HertzBeat community, there are many ways to contribute: + +- 💻**Code**: Can help the community complete some tasks, write new features or fix some bugs; + +- ⚠️**Test**: Can come to participate in the writing of test code, including unit testing, integration testing, e2e testing; + +- 📖**Docs**: Can write or Documentation improved to help users better understand and use HertzBeat; + +- 📝**Blog**: You can write articles about HertzBeat to help the community better promote; + +- 🤔**Discussion**: You can participate in the discussion of new features of HertzBeat and integrate your ideas with HertzBeat; + +- 💡**Preach**: Can help publicize or promote the HertzBeat community, speak in meetup or summit; + +- 💬**Suggestion**: You can also make some suggestions to the project or community to promote the healthy development of the community; + +More see [Contribution Types](https://allcontributors.org/en/reference/emoji-key/) + +Even small corrections to typos are very welcome :) + +### Getting HertzBeat up and running + +> To get HertzBeat code running on your development tools, and able to debug with breakpoints. +> This is a front-end and back-end separation project. To start the local code, the back-end [startup](hertzbeat-startup) and the front-end [web-app](web-app) must be started separately. + + +#### Backend start + +1. Requires `maven3+`, `java25` and `lombok` environments + +2. (Optional) Modify the configuration file: `hertzbeat-startup/src/main/resources/application.yml` + +3. Execute under the project root directory: `mvn clean install -DskipTests` + +4. Add VM Options: `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` + +5. Start `springboot startup` service: `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java` + +#### Frontend start + +1. Need `Node pnpm` Environment, Make sure `Node.js >= 18` + +2. Cd to the `web-app` directory: `cd web-app` + +3. Install pnpm if not existed `npm install -g pnpm` + +4. Install Dependencies: `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app` + +5. After the local backend is started, start the local frontend in the web-app directory: `pnpm start` + +6. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat* + + +### Find tasks + +Find the issue you are interested in! On our GitHub repo issue list, we often publish some issues with the label good first issue or status: volunteer wanted. +These issues welcome the help of contributors. Among them, good first issues tend to have low thresholds and are suitable for novices. + +Of course, if you have a good idea, you can also propose it directly on GitHub Discussion or contact with community. + +### Submit Pull Request + +1. First you need to fork your target [hertzbeat repository](https://github.com/apache/hertzbeat). +2. Then download the code locally with git command: + ```shell + git clone git@github.com:${YOUR_USERNAME}/hertzbeat.git #Recommended + ``` +3. After the download is complete, please refer to the getting started guide or README file of the target repository to initialize the project. +4. Then, you can refer to the following command to submit the code: + ```shell + git checkout -b a-feature-branch #Recommended + ``` +5. Submit the code as a commit, the commit message format specification required: [module name or type name]feature or bugfix or doc: custom message. + ```shell + git add <modified file/path> + git commit -m '[docs]feature: necessary instructions' #Recommended + ``` +6. Push to the remote repository + ```shell + git push origin a-feature-branch + ``` +7. Then you can initiate a new PR (Pull Request) on GitHub. + +Please note that the title of the PR needs to conform to our spec, and write the necessary description in the PR to facilitate code review by Committers and other contributors. + +### Wait for the code to be merged + +After submitting the PR, the Committee or the community's friends will review the code you submitted (Code Review), and will propose some modification suggestions or conduct some discussions. Please pay attention to your PR in time. + +If subsequent changes are required, there is no need to initiate a new PR. After submitting a commit on the original branch and pushing it to the remote repository, the PR will be automatically updated. + +In addition, our project has a relatively standardized and strict CI inspection process. After submitting PR, CI will be triggered. Please pay attention to whether it passes the CI inspection. + +Finally, the Committers can merge the PR into the master branch. + +### After the code is merged + +After the code has been merged, you can delete the development branch on both the local and remote repositories: + +```shell +git branch -d a-dev-branch +git push origin --delete a-dev-branch +``` + +On the master/main branch, you can do the following to sync the upstream repository: + +```shell +git remote add upstream https://github.com/apache/hertzbeat.git #Bind the remote warehouse, if it has been executed, it does not need to be executed again +git checkout master +git pull upstream master +``` + +### HertzBeat Improvement Proposal (HIP) +If you have major new features(e.g., support metrics push gateway, support logs monitoring), you need to write a design document known as a HertzBeat Improvement Proposal (HIP). Before starting to write a HIP, make sure you follow the process [here](https://github.com/apache/hertzbeat/tree/master/hip). + +### How to become a Committer? + +With the above steps, you are a contributor to HertzBeat. Repeat the previous steps to stay active in the community, keep at, you can become a Committer! + +### Join Discussion + +[Join the Mailing Lists](https://lists.apache.org/list.html?dev@hertzbeat.apache.org) : Mail to `dev-subscribe@hertzbeat.apache.org` to subscribe mailing lists. + +Add WeChat account `ahertzbeat` to pull you into the WeChat group. + +## 🥐 Architecture + +![hertzBeat](home/static/img/docs/hertzbeat-arch.png) + +<br> + +------ + +<br> + +## 贡献者指南 + +> 非常欢迎参与项目贡献,我们致力于维护一个互相帮助的快乐社区。 + +### 贡献方式 + +> 在 HertzBeat 社区,贡献方式有很多: + +- 💻**代码**:可以帮助社区完成一些任务、编写新的feature或者是修复一些bug; + +- ⚠️**测试**:可以来参与测试代码的编写,包括了单元测试、集成测试、e2e测试; + +- 📖**文档**:可以编写或完善文档,来帮助用户更好地了解和使用 HertzBeat; + +- 📝**博客**:可以撰写 HertzBeat 的相关文章,来帮助社区更好地推广; + +- 🤔**讨论**:可以参与 HertzBeat 新的feature的讨论,将您的想法跟 HertzBeat 融合; + +- 💡**布道**:可以帮助宣传或推广 HertzBeat 社区,在 meetup 或 summit 中演讲; + +- 💬**建议**:也可以对项目或者社区提出一些建议,促进社区的良性发展; + +更多贡献方式参见 [Contribution Types](https://allcontributors.org/en/reference/emoji-key/) + +即便是小到错别字的修正我们也都非常欢迎 :) + +### 让 HertzBeat 运行起来 + +> 让 HertzBeat 的代码在您的开发工具上运行起来,并且能够断点调试。 +> 此为前后端分离项目,本地代码启动需将后端[hertzbeat-startup](hertzbeat-startup)和前端[web-app](web-app)分别启动生效。 + +#### 后端启动 + +1. 需要 `maven3+`, `java25` 和 `lombok` 环境 + +2. (可选)修改配置文件配置信息-`hertzbeat-startup/src/main/resources/application.yml` + +3. 在项目根目录运行编译: `mvn clean install -DskipTests` + +4. 在 `jvm` 加入参数 `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` + +5. 启动`springboot startup`服务 `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java` + + +#### 前端启动 + +1. 需要 `nodejs pnpm` 环境, Make sure `Node.js >= 18` + +2. 进入 `web-app` 目录: `cd web-app` + +3. 安装pnpm: `npm install -g pnpm` + +4. 在前端工程目录 `web-app` 下执行: `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app` + +5. 待本地后端启动后,在web-app目录下启动本地前端 `pnpm start` + +6. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat* + +### 寻找任务 + +寻找您感兴趣的Issue!在我们的GitHub仓库和邮件列表中,我们经常会发布一些带有 good first issue 或者 status: volunteer wanted 标签的issue,这些issue都欢迎贡献者的帮助。 +其中good first issue往往门槛较低、适合新手。 + +当然,如果您有好的想法,也可以直接在GitHub Discussion 中提出或者联系社区。 + +### 提交 Pull Request + +1. 首先您需要 Fork 目标仓库 [hertzbeat repository](https://github.com/apache/hertzbeat). +2. 然后 用git命令 将代码下载到本地: + ```shell + git clone git@github.com:${YOUR_USERNAME}/hertzbeat.git #Recommended + ``` +3. 下载完成后,请参考目标仓库的入门指南或者 README 文件对项目进行初始化。 +4. 接着,您可以参考如下命令进行代码的提交, 切换新的分支, 进行开发: + ```shell + git checkout -b a-feature-branch #Recommended + ``` +5. 提交 commit , commit 描述信息需要符合约定格式: [module name or type name]feature or bugfix or doc: custom message. + ```shell + git add <modified file/path> + git commit -m '[docs]feature: necessary instructions' #Recommended + ``` +6. 推送到远程仓库 + ```shell + git push origin a-feature-branch + ``` +7. 然后您就可以在 GitHub 上发起新的 PR (Pull Request)。 + +请注意 PR 的标题需要符合我们的规范,并且在 PR 中写上必要的说明,来方便 Committer 和其他贡献者进行代码审查。 + +### 等待PR代码被合并 + +在提交了 PR 后,Committer 或者社区的小伙伴们会对您提交的代码进行审查(Code Review),会提出一些修改建议,或者是进行一些讨论,请及时关注您的PR。 + +若后续需要改动,不需要发起一个新的 PR,在原有的分支上提交 commit 并推送到远程仓库后,PR会自动更新。 + +另外,我们的项目有比较规范和严格的 CI 检查流程,在提交 PR 之后会触发 CI,请注意是否通过 CI 检查。 + +最后,Committer 可以将 PR 合并入 master 主分支。 + +### 代码被合并后 + +在代码被合并后,您就可以在本地和远程仓库删除这个开发分支了: + +```shell +git branch -d a-dev-branch +git push origin --delete a-dev-branch +``` + +在主分支上,您可以执行以下操作来同步上游仓库: + +```shell +git remote add upstream https://github.com/apache/hertzbeat.git #Bind the remote warehouse, if it has been executed, it does not need to be executed again +git checkout master +git pull upstream master +``` + +### HertzBeat 改进提案 (HIP) +如果您有重大的新特性(例如,支持指标推送网关,日志监控等),您需要编写一个被称为 HertzBeat 改进提案(HertzBeat Improvement Proposal,HIP)的设计文档。在开始编写 HIP 之前,请确保您遵循了[这里](https://github.com/apache/hertzbeat/tree/master/hip)的流程。 + +### 如何成为Committer? + +通过上述步骤,您就是 HertzBeat 的贡献者了。重复前面的步骤,在社区中保持活跃,坚持下去,您就能成为 Committer! + +### 加入讨论交流 + +[Join the Mailing Lists](https://lists.apache.org/list.html?dev@hertzbeat.apache.org) : Mail to `dev-subscribe@hertzbeat.apache.org` to subscribe mailing lists. + +Add WeChat account `ahertzbeat` to pull you into the WeChat group. + +### 模块 + +![hertzBeat](home/static/img/docs/hertzbeat-arch.png) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..415987b --- /dev/null +++ b/LICENSE @@ -0,0 +1,237 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + +======================================================================== +MIT License +======================================================================== + +The following components are provided under the MIT License +These files from https://github.com/ng-alain/ng-alain +The text of the license is included in material/licenses/frontend/LICENSE-ng-alain.txt + +web-app/src/app/core/** +web-app/src/app/layout/** +web-app/src/app/routes/exception/** +web-app/src/app/routes/routes.module.ts +web-app/src/app/routes/routes-routing.module.ts +web-app/src/app/shared/json-schema/** +web-app/src/app/shared/index.ts +web-app/src/app/shared/shared.module.ts +web-app/src/app/shared/shared-delon.module.ts +web-app/src/app/shared/shared-zorro.module.ts +web-app/src/app/app.module.ts +web-app/src/app/app.component.ts +web-app/src/app/global-config.module.ts +web-app/src/app/icons-provider.module.ts +web-app/src/assets/color.less +web-app/src/assets/style.compact.less +web-app/src/assets/style.dark.less +web-app/src/environments/** +web-app/src/styles/** +web-app/src/index.html +web-app/src/main.ts +web-app/src/polyfills.ts +web-app/src/style-icons.ts +web-app/src/style-icons-auto.ts +web-app/src/style.less +web-app/src/test.ts +web-app/src/typings.d.ts diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..37d0c72 --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +Apache HertzBeat™ +Copyright 2024-2025 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2a2cd8 --- /dev/null +++ b/README.md @@ -0,0 +1,598 @@ +<p align="center"> + <a href="https://hertzbeat.apache.org"> + <img alt="hertzbeat" src="/home/static/img/hertzbeat-brand.svg" width="260"> + </a> +</p> + +<p align="center"> +<b>Readme</b>: +<b><a href="README.md">English</a></b> | <a href="README_CN.md">中文</a> | <a href="README_JP.md">日本語</a> +</p> + +[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr) +[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://x.com/hertzbeat1024) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8139/badge)](https://www.bestpractices.dev/projects/8139) +[![codecov](https://codecov.io/gh/apache/HertzBeat/branch/master/graph/badge.svg)](https://app.codecov.io/gh/apache/hertzbeat) +[![Docker Pulls](https://img.shields.io/docker/pulls/apache/hertzbeat?style=%20for-the-badge&logo=docker&label=DockerHub%20Download)](https://hub.docker.com/r/apache/hertzbeat) +[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/hertzbeat)](https://artifacthub.io/packages/search?repo=hertzbeat) +[![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCri75zfWX0GHqJFPENEbLow?logo=youtube&label=YouTube%20Channel)](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow) +[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/apache/hertzbeat) +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/apache/hertzbeat) +[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/apache/hertzbeat) + +**Home: [hertzbeat.apache.org](https://hertzbeat.apache.org)** +**Email: <a href="mailto:dev-subscribe@hertzbeat.apache.org">Mail to `dev-subscribe@hertzbeat.apache.org`</a> to subscribe mailing lists** + + +## 🎡 <font color="green">Introduction</font> + +[Apache HertzBeat™](https://github.com/apache/hertzbeat) is an AI-powered next-generation open source real-time observability system. Unified metrics and logs collection, centralized alerting distribution, intelligent management and analysis. No Agent required, high performance cluster, provides powerful custom monitoring and status page building capabilities. + +### Features + +- Integrates **collection + analysis + alerting + notification** into one platform, with new AI-powered interactions and features under HertzBeat AI, and built-in MCP Server capabilities. +- Unified metrics platform, agentless, Prometheus-compatible, supports application services, programs, databases, caches, operating systems, big data, middleware, web servers, cloud-native, networks, custom monitoring and more. +- Unified logging platform, seamlessly integrates multiple log sources through OTLP protocol for reporting. +- Unified alerting platform, integrates internal alerts with various external alert sources, unified alert processing and analysis, flexible real-time and periodic threshold rules, grouping convergence, silence, suppression, etc. +- Unified message distribution, alerts processed by the alerting platform are distributed via `Email` `Discord` `Slack` `Telegram` `DingTalk` `WeChat` `FeiShu` `SMS` `Webhook` `ServerChan` and other methods. +- Makes protocols such as `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` configurable, allowing you to collect any metrics by simply configuring the template `YML` file online. Imagine being able to quickly adapt to a new monitoring type like K8s or Docker simply by configuring online with HertzBeat. +- High performance, supports horizontal expansion of multi-collector clusters, multi-isolated network monitoring and cloud-edge collaboration. +- Provides powerful status page building capabilities, easily communicate the real-time status of your service to users. + +> HertzBeat's unified platform, AI intelligence, powerful customization, multi-type support, high performance, and easy expansion, aims to help users quickly and conveniently achieve observability requirements. + +---- + +---- + +## 🥐 Architecture + +![HertzBeat](home/static/img/docs/hertzbeat-arch.png) + +## ⛄ 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. + +### 🍞 Install HertzBeat + +> HertzBeat supports installation through source code, docker or package, cpu support x86/arm64. + +##### 1:Install quickly via docker + +1. Just one command to get started + + ```shell + docker run -d -p 1157:1157 -p 1158:1158 --name hertzbeat apache/hertzbeat + ``` + +2. Access `http://localhost:1157` to start, default account: `admin/hertzbeat` + +3. Deploy collector clusters (Optional) + + ```shell + docker run -d -e IDENTITY=custom-collector-name -e MANAGER_HOST=127.0.0.1 -e MANAGER_PORT=1158 --name hertzbeat-collector apache/hertzbeat-collector + ``` + + - `-e IDENTITY=custom-collector-name` : set the collector unique identity name. + - `-e MODE=public` : set the running mode(public or private), public cluster or private cloud-edge. + - `-e MANAGER_HOST=127.0.0.1` : set the main hertzbeat server ip. + - `-e MANAGER_PORT=1158` : set the main hertzbeat server port, default 1158. + + +Detailed config refer to [Install HertzBeat via Docker](https://hertzbeat.apache.org/docs/start/docker-deploy) + +##### 2:Install via package + +1. Download the release package `apache-hertzbeat-xx-bin.tar.gz` [Download](https://hertzbeat.apache.org/docs/download) +2. Configure the HertzBeat configuration yml file `hertzbeat/config/application.yml` (optional) +3. Run command `$ ./bin/startup.sh ` or `bin/startup.bat` +4. Access `http://localhost:1157` to start, default account: `admin/hertzbeat` +5. Deploy collector clusters (Optional) + - Download the release package `apache-hertzbeat-collector-xx-bin.tar.gz` (JVM collector) or the native collector package for your platform, such as `apache-hertzbeat-collector-native-xx-linux-amd64-bin.tar.gz` or `apache-hertzbeat-collector-native-xx-windows-amd64-bin.zip`, to the new machine [Download](https://hertzbeat.apache.org/docs/download) + - Configure the collector configuration yml file `hertzbeat-collector/config/application.yml`: unique `identity` name, running `mode` (public or private), hertzbeat `manager-host`, hertzbeat `manager-port` + ```yaml + collector: + dispatch: + entrance: + netty: + enabled: true + identity: ${IDENTITY:} + mode: ${MODE:public} + manager-host: ${MANAGER_HOST:127.0.0.1} + manager-port: ${MANAGER_PORT:1158} + ``` + - If you do not provide JDBC drivers in `ext-lib`, MySQL, MariaDB, and OceanBase can use the built-in query engine and run on the native collector package as well. TiDB follows the same rule for its SQL query metric set. + - If `mysql-connector-j` is present in `ext-lib`, the built-in server collector or JVM collector automatically prefers JDBC after restart for MySQL, MariaDB, and OceanBase. TiDB follows the same rule for its SQL query metric set, while its HTTP metrics are unchanged. Oracle and DB2 still require the JVM collector package because they depend on external JDBC drivers. + - Run `$ ./bin/startup.sh ` or `bin/startup.bat` for the JVM collector package. Run `$ ./bin/startup.sh ` for Linux or macOS native collector packages, and `bin\\startup.bat` for the Windows native collector package. + - Access `http://localhost:1157` and you will see the registered new collector in dashboard + +Detailed config refer to [Install HertzBeat via Package](https://hertzbeat.apache.org/docs/start/package-deploy) + +##### 3:Start via source code + +1. Local source code debugging needs to start the back-end project `hertzbeat-startup` and the front-end project `web-app`. +2. Backend:need `maven3+`, `java25`, `lombok`, add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `hertzbeat-startup` service. +3. Web:need `nodejs` and `pnpm` environment, run `pnpm install` then `pnpm start` in `web-app` directory after backend startup. +4. Access `http://localhost:4200` to start, default account: `admin/hertzbeat` + +Detailed steps refer to [CONTRIBUTING](CONTRIBUTING.md) + +##### 4:Install All(hertzbeat+postgresql+tsdb) via Docker-compose + +Install the postgresql/mysql database, victoria-metrics/iotdb/tdengine database and hertzbeat at one time through [docker-compose deployment script](script/docker-compose). + +Detailed steps refer to [Install via Docker-Compose](script/docker-compose/README.md) + +##### 5: Install All(hertzbeat+collector+postgresql+tsdb) via kubernetes helm charts + +Install HertzBeat cluster in a Kubernetes cluster by Helm chart. + +Detailed steps refer to [Artifact Hub](https://artifacthub.io/packages/helm/hertzbeat/hertzbeat) + +**HAVE FUN** + +## ✨ Contributors + +Thanks to these wonderful people, welcome to join us: +[Contributor Guide](CONTRIBUTING.md) + +<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> +<!-- prettier-ignore-start --> +<!-- markdownlint-disable --> +<table> + <tbody> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/tomsun28"><img src="https://avatars.githubusercontent.com/u/24788200?v=4?s=100" width="100px;" alt="tomsun28"/><br /><sub><b>tomsun28</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Documentation">📖</a> <a href="#design-tomsun28" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wang1027-wqh"><img src="https://avatars.githubusercontent.com/u/71161318?v=4?s=100" width="100px;" alt="会编程的王学长"/><br /><sub><b>会编程的王学长</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Documentation">📖</a> <a href="#design-wang1027-wqh" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.maxkey.top/"><img src="https://avatars.githubusercontent.com/u/1563377?v=4?s=100" width="100px;" alt="MaxKey"/><br /><sub><b>MaxKey</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shimingxy" title="Code">💻</a> <a href="#design-shimingxy" title="Design">🎨</a> <a href="#ideas-shimingxy" title="Ideas, Planning, & Feedback">🤔</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://blog.gcdd.top/"><img src="https://avatars.githubusercontent.com/u/26523525?v=4?s=100" width="100px;" alt="观沧海"/><br /><sub><b>观沧海</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gcdd1993" title="Code">💻</a> <a href="#design-gcdd1993" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agcdd1993" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/a25017012"><img src="https://avatars.githubusercontent.com/u/32265356?v=4?s=100" width="100px;" alt="yuye"/><br /><sub><b>yuye</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jx10086"><img src="https://avatars.githubusercontent.com/u/5323228?v=4?s=100" width="100px;" alt="jx10086"/><br /><sub><b>jx10086</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx10086" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ajx10086" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/winnerTimer"><img src="https://avatars.githubusercontent.com/u/76024658?v=4?s=100" width="100px;" alt="winnerTimer"/><br /><sub><b>winnerTimer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=winnerTimer" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AwinnerTimer" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/goo-kits"><img src="https://avatars.githubusercontent.com/u/13163673?v=4?s=100" width="100px;" alt="goo-kits"/><br /><sub><b>goo-kits</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=goo-kits" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agoo-kits" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/brave4Time"><img src="https://avatars.githubusercontent.com/u/105094014?v=4?s=100" width="100px;" alt="brave4Time"/><br /><sub><b>brave4Time</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brave4Time" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abrave4Time" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/walkerlee-lab"><img src="https://avatars.githubusercontent.com/u/8426753?v=4?s=100" width="100px;" alt="WalkerLee"/><br /><sub><b>WalkerLee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=walkerlee-lab" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Awalkerlee-lab" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/fullofjoy"><img src="https://avatars.githubusercontent.com/u/30247571?v=4?s=100" width="100px;" alt="jianghang"/><br /><sub><b>jianghang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fullofjoy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Afullofjoy" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ChineseTony"><img src="https://avatars.githubusercontent.com/u/24618786?v=4?s=100" width="100px;" alt="ChineseTony"/><br /><sub><b>ChineseTony</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChineseTony" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AChineseTony" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt199905"><img src="https://avatars.githubusercontent.com/u/85098809?v=4?s=100" width="100px;" alt="wyt199905"/><br /><sub><b>wyt199905</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt199905" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/weifuqing"><img src="https://avatars.githubusercontent.com/u/13931013?v=4?s=100" width="100px;" alt="卫傅庆"/><br /><sub><b>卫傅庆</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weifuqing" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aweifuqing" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zklmcookle"><img src="https://avatars.githubusercontent.com/u/107192352?v=4?s=100" width="100px;" alt="zklmcookle"/><br /><sub><b>zklmcookle</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zklmcookle" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/DevilX5"><img src="https://avatars.githubusercontent.com/u/13269921?v=4?s=100" width="100px;" alt="DevilX5"/><br /><sub><b>DevilX5</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/djzeng"><img src="https://avatars.githubusercontent.com/u/14074864?v=4?s=100" width="100px;" alt="tea"/><br /><sub><b>tea</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=djzeng" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yangshihui"><img src="https://avatars.githubusercontent.com/u/28550208?v=4?s=100" width="100px;" alt="yangshihui"/><br /><sub><b>yangshihui</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yangshihui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayangshihui" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/DreamGirl524"><img src="https://avatars.githubusercontent.com/u/81132838?v=4?s=100" width="100px;" alt="DreamGirl524"/><br /><sub><b>DreamGirl524</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/gzwlly"><img src="https://avatars.githubusercontent.com/u/83171907?v=4?s=100" width="100px;" alt="gzwlly"/><br /><sub><b>gzwlly</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gzwlly" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/cuipiheqiuqiu"><img src="https://avatars.githubusercontent.com/u/76642201?v=4?s=100" width="100px;" alt="cuipiheqiuqiu"/><br /><sub><b>cuipiheqiuqiu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Tests">⚠️</a> <a href="#design-cuipiheqiuqiu" title="Design">🎨</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/oyiyou"><img src="https://avatars.githubusercontent.com/u/39228891?v=4?s=100" width="100px;" alt="lambert"/><br /><sub><b>lambert</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=oyiyou" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://mroldx.xyz/"><img src="https://avatars.githubusercontent.com/u/34847828?v=4?s=100" width="100px;" alt="mroldx"/><br /><sub><b>mroldx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mroldx" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/woshiniusange"><img src="https://avatars.githubusercontent.com/u/91513022?v=4?s=100" width="100px;" alt="woshiniusange"/><br /><sub><b>woshiniusange</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=woshiniusange" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://vampireachao.github.io/"><img src="https://avatars.githubusercontent.com/u/52746628?v=4?s=100" width="100px;" alt="VampireAchao"/><br /><sub><b>VampireAchao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=VampireAchao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Ceilzcx"><img src="https://avatars.githubusercontent.com/u/48920254?v=4?s=100" width="100px;" alt="zcx"/><br /><sub><b>zcx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACeilzcx" title="Bug reports">🐛</a> <a href="#design-Ceilzcx" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Tests">⚠️</a> <a href="#blog-Ceilzcx" title="Blogposts">📝</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieXCL"><img src="https://avatars.githubusercontent.com/u/91540487?v=4?s=100" width="100px;" alt="CharlieXCL"/><br /><sub><b>CharlieXCL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieXCL" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Privauto"><img src="https://avatars.githubusercontent.com/u/36581456?v=4?s=100" width="100px;" alt="Privauto"/><br /><sub><b>Privauto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/emrys-he"><img src="https://avatars.githubusercontent.com/u/5848915?v=4?s=100" width="100px;" alt="emrys"/><br /><sub><b>emrys</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=emrys-he" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/SxLiuYu"><img src="https://avatars.githubusercontent.com/u/95198625?v=4?s=100" width="100px;" alt="SxLiuYu"/><br /><sub><b>SxLiuYu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ASxLiuYu" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://allcontributors.org"><img src="https://avatars.githubusercontent.com/u/46410174?v=4?s=100" width="100px;" alt="All Contributors"/><br /><sub><b>All Contributors</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=all-contributors" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/gxc-myh"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/click33"><img src="https://avatars.githubusercontent.com/u/36243476?v=4?s=100" width="100px;" alt="click33"/><br /><sub><b>click33</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=click33" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://jpom.io/"><img src="https://avatars.githubusercontent.com/u/16408873?v=4?s=100" width="100px;" alt="蒋小小"/><br /><sub><b>蒋小小</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bwcx-jzy" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.zhihu.com/people/kevinbauer"><img src="https://avatars.githubusercontent.com/u/28581579?v=4?s=100" width="100px;" alt="Kevin Huang"/><br /><sub><b>Kevin Huang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=kevinhuangwl" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/TJxiaobao"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ATJxiaobao" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Tests">⚠️</a> <a href="#design-TJxiaobao" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Jack-123-power"><img src="https://avatars.githubusercontent.com/u/84333501?v=4?s=100" width="100px;" alt="Captain Jack"/><br /><sub><b>Captain Jack</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jack-123-power" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/haibo-duan"><img src="https://avatars.githubusercontent.com/u/7974845?v=4?s=100" width="100px;" alt="haibo.duan"/><br /><sub><b>haibo.duan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/assassinfym"><img src="https://avatars.githubusercontent.com/u/15188754?v=4?s=100" width="100px;" alt="assassin"/><br /><sub><b>assassin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Aassassinfym" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=assassinfym" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/csyshu"><img src="https://avatars.githubusercontent.com/u/46591658?v=4?s=100" width="100px;" alt="Reverse wind"/><br /><sub><b>Reverse wind</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/luxx-lq"><img src="https://avatars.githubusercontent.com/u/58515565?v=4?s=100" width="100px;" alt="luxx"/><br /><sub><b>luxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luxx-lq" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eltociear" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zenan08"><img src="https://avatars.githubusercontent.com/u/80514991?v=4?s=100" width="100px;" alt="leizenan"/><br /><sub><b>leizenan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zenan08" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/BKing2020"><img src="https://avatars.githubusercontent.com/u/28869121?v=4?s=100" width="100px;" alt="BKing"/><br /><sub><b>BKing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BKing2020" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xingshuaiLi"><img src="https://avatars.githubusercontent.com/u/119487588?v=4?s=100" width="100px;" alt="xingshuaiLi"/><br /><sub><b>xingshuaiLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xingshuaiLi" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wangke6666"><img src="https://avatars.githubusercontent.com/u/113656595?v=4?s=100" width="100px;" alt="wangke6666"/><br /><sub><b>wangke6666</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wangke6666" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LWBobo"><img src="https://avatars.githubusercontent.com/u/50368698?v=4?s=100" width="100px;" alt="刺猬"/><br /><sub><b>刺猬</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ALWBobo" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LWBobo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.zanglikun.com"><img src="https://avatars.githubusercontent.com/u/61591648?v=4?s=100" width="100px;" alt="Haste"/><br /><sub><b>Haste</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zanglikun" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/SuitSmile"><img src="https://avatars.githubusercontent.com/u/38679717?v=4?s=100" width="100px;" alt="zhongshi.yi"/><br /><sub><b>zhongshi.yi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SuitSmile" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://www.smallq.cn"><img src="https://avatars.githubusercontent.com/u/39754275?v=4?s=100" width="100px;" alt="Qi Zhang"/><br /><sub><b>Qi Zhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zzzhangqi" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MrAndyMing"><img src="https://avatars.githubusercontent.com/u/49541483?v=4?s=100" width="100px;" alt="MrAndyMing"/><br /><sub><b>MrAndyMing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrAndyMing" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://idongliming.github.io/"><img src="https://avatars.githubusercontent.com/u/31564353?v=4?s=100" width="100px;" alt="idongliming"/><br /><sub><b>idongliming</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=idongliming" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://earthjasonlin.github.io"><img src="https://avatars.githubusercontent.com/u/83632110?v=4?s=100" width="100px;" alt="Zichao Lin"/><br /><sub><b>Zichao Lin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://blog.liudonghua.com"><img src="https://avatars.githubusercontent.com/u/2276718?v=4?s=100" width="100px;" alt="liudonghua"/><br /><sub><b>liudonghua</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liudonghua123" title="Code">💻</a> <a href="#ideas-liudonghua123" title="Ideas, Planning, & Feedback">🤔</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeyts"><img src="https://avatars.githubusercontent.com/u/4250869?v=4?s=100" width="100px;" alt="Jerry"/><br /><sub><b>Jerry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Tests">⚠️</a> <a href="#ideas-orangeyts" title="Ideas, Planning, & Feedback">🤔</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://dynamictp.cn"><img src="https://avatars.githubusercontent.com/u/13051908?v=4?s=100" width="100px;" alt="yanhom"/><br /><sub><b>yanhom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yanhom1314" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://www.jianshu.com/u/a8f822c04f67"><img src="https://avatars.githubusercontent.com/u/18587688?v=4?s=100" width="100px;" alt="fsl"/><br /><sub><b>fsl</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengshunli" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xttttv"><img src="https://avatars.githubusercontent.com/u/116323904?v=4?s=100" width="100px;" alt="xttttv"/><br /><sub><b>xttttv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xttttv" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/NavinKumarBarnwal"><img src="https://avatars.githubusercontent.com/u/44504274?v=4?s=100" width="100px;" alt="NavinKumarBarnwal"/><br /><sub><b>NavinKumarBarnwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NavinKumarBarnwal" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/z641205699"><img src="https://avatars.githubusercontent.com/u/45276423?v=4?s=100" width="100px;" alt="Zakkary"/><br /><sub><b>Zakkary</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=z641205699" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/898349230"><img src="https://avatars.githubusercontent.com/u/21972532?v=4?s=100" width="100px;" alt="sunxinbo"/><br /><sub><b>sunxinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ldzbook"><img src="https://avatars.githubusercontent.com/u/13903790?v=4?s=100" width="100px;" alt="ldzbook"/><br /><sub><b>ldzbook</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldzbook" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aldzbook" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/SurryChen"><img src="https://avatars.githubusercontent.com/u/91116490?v=4?s=100" width="100px;" alt="余与雨"/><br /><sub><b>余与雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Tests">⚠️</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MysticalDream"><img src="https://avatars.githubusercontent.com/u/78899028?v=4?s=100" width="100px;" alt="MysticalDream"/><br /><sub><b>MysticalDream</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zhouyoulin12"><img src="https://avatars.githubusercontent.com/u/17086633?v=4?s=100" width="100px;" alt="zhouyoulin12"/><br /><sub><b>zhouyoulin12</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jerjjj"><img src="https://avatars.githubusercontent.com/u/93431283?v=4?s=100" width="100px;" alt="jerjjj"/><br /><sub><b>jerjjj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jerjjj" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://wjl110.xyz/"><img src="https://avatars.githubusercontent.com/u/53851034?v=4?s=100" width="100px;" alt="wjl110"/><br /><sub><b>wjl110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wjl110" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ngyhd"><img src="https://avatars.githubusercontent.com/u/29095207?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ngyhd" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Daydreamer-ia"><img src="https://avatars.githubusercontent.com/u/83362909?v=4?s=100" width="100px;" alt="chenyiqin"/><br /><sub><b>chenyiqin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/hudongdong129"><img src="https://avatars.githubusercontent.com/u/34374227?v=4?s=100" width="100px;" alt="hudongdong129"/><br /><sub><b>hudongdong129</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Documentation">📖</a> <a href="#design-hudongdong129" title="Design">🎨</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/TherChenYang"><img src="https://avatars.githubusercontent.com/u/124348939?v=4?s=100" width="100px;" alt="TherChenYang"/><br /><sub><b>TherChenYang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/HattoriHenzo"><img src="https://avatars.githubusercontent.com/u/5141285?v=4?s=100" width="100px;" alt="HattoriHenzo"/><br /><sub><b>HattoriHenzo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ycilry"><img src="https://avatars.githubusercontent.com/u/63967101?v=4?s=100" width="100px;" alt="ycilry"/><br /><sub><b>ycilry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ycilry" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/aoshiguchen"><img src="https://avatars.githubusercontent.com/u/10580997?v=4?s=100" width="100px;" alt="aoshiguchen"/><br /><sub><b>aoshiguchen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/caibenxiang"><img src="https://avatars.githubusercontent.com/u/4568241?v=4?s=100" width="100px;" alt="蔡本祥"/><br /><sub><b>蔡本祥</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caibenxiang" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.fckeverything.cn:4000/"><img src="https://avatars.githubusercontent.com/u/13827124?v=4?s=100" width="100px;" alt="浮游"/><br /><sub><b>浮游</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lifefloating" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Grass-Life"><img src="https://avatars.githubusercontent.com/u/114381513?v=4?s=100" width="100px;" alt="Grass-Life"/><br /><sub><b>Grass-Life</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Grass-Life" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaohe428"><img src="https://avatars.githubusercontent.com/u/99130317?v=4?s=100" width="100px;" alt="xiaohe428"/><br /><sub><b>xiaohe428</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/baiban114"><img src="https://avatars.githubusercontent.com/u/59152619?v=4?s=100" width="100px;" alt="TableRow"/><br /><sub><b>TableRow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ByteIDance"><img src="https://avatars.githubusercontent.com/u/100207562?v=4?s=100" width="100px;" alt="ByteIDance"/><br /><sub><b>ByteIDance</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ByteIDance" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/mangel2002"><img src="https://avatars.githubusercontent.com/u/9348020?v=4?s=100" width="100px;" alt="Jangfe"/><br /><sub><b>Jangfe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mangel2002" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zqr10159"><img src="https://avatars.githubusercontent.com/u/30048352?v=4?s=100" width="100px;" alt="zqr10159"/><br /><sub><b>zqr10159</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Code">💻</a> <a href="#blog-zqr10159" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azqr10159" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Tests">⚠️</a> <a href="#design-zqr10159" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/vinci-897"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="vinci"/><br /><sub><b>vinci</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Documentation">📖</a> <a href="#design-vinci-897" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/js110"><img src="https://avatars.githubusercontent.com/u/51191863?v=4?s=100" width="100px;" alt="js110"/><br /><sub><b>js110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=js110" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/JavaLionLi"><img src="https://avatars.githubusercontent.com/u/31852897?v=4?s=100" width="100px;" alt="CrazyLionLi"/><br /><sub><b>CrazyLionLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaLionLi" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.banmajio.com"><img src="https://avatars.githubusercontent.com/u/53471385?v=4?s=100" width="100px;" alt="banmajio"/><br /><sub><b>banmajio</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=banmajio" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://suder.fun"><img src="https://avatars.githubusercontent.com/u/69955165?v=4?s=100" width="100px;" alt="topsuder"/><br /><sub><b>topsuder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=topsuder" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/richar2022"><img src="https://avatars.githubusercontent.com/u/129016397?v=4?s=100" width="100px;" alt="richar2022"/><br /><sub><b>richar2022</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=richar2022" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/fcb-xiaobo"><img src="https://avatars.githubusercontent.com/u/60566194?v=4?s=100" width="100px;" alt="fcb-xiaobo"/><br /><sub><b>fcb-xiaobo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fcb-xiaobo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wenkyzhang"><img src="https://avatars.githubusercontent.com/u/13983669?v=4?s=100" width="100px;" alt="wenkyzhang"/><br /><sub><b>wenkyzhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wenkyzhang" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ZangJuxy"><img src="https://avatars.githubusercontent.com/u/71380295?v=4?s=100" width="100px;" alt="ZangJuxy"/><br /><sub><b>ZangJuxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZangJuxy" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/l646505418"><img src="https://avatars.githubusercontent.com/u/50475131?v=4?s=100" width="100px;" alt="l646505418"/><br /><sub><b>l646505418</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=l646505418" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Al646505418" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.carpewang.com"><img src="https://avatars.githubusercontent.com/u/78642589?v=4?s=100" width="100px;" alt="Carpe-Wang"/><br /><sub><b>Carpe-Wang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Carpe-Wang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACarpe-Wang" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/moshu023"><img src="https://avatars.githubusercontent.com/u/48593205?v=4?s=100" width="100px;" alt="莫枢"/><br /><sub><b>莫枢</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moshu023" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/huangcanda"><img src="https://avatars.githubusercontent.com/u/4470566?v=4?s=100" width="100px;" alt="huangcanda"/><br /><sub><b>huangcanda</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=huangcanda" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.zrkizzy.com"><img src="https://avatars.githubusercontent.com/u/85340613?v=4?s=100" width="100px;" alt="世纪末的架构师"/><br /><sub><b>世纪末的架构师</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Architect-Java" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ShuningWan"><img src="https://avatars.githubusercontent.com/u/31086770?v=4?s=100" width="100px;" alt="ShuningWan"/><br /><sub><b>ShuningWan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShuningWan" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MrYZhou"><img src="https://avatars.githubusercontent.com/u/44339602?v=4?s=100" width="100px;" alt="MrYZhou"/><br /><sub><b>MrYZhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrYZhou" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/suncqujsj"><img src="https://avatars.githubusercontent.com/u/8012932?v=4?s=100" width="100px;" alt="suncqujsj"/><br /><sub><b>suncqujsj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=suncqujsj" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/sunqinbo"><img src="https://avatars.githubusercontent.com/u/1428540?v=4?s=100" width="100px;" alt="sunqinbo"/><br /><sub><b>sunqinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sunqinbo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/haoww"><img src="https://avatars.githubusercontent.com/u/32739294?v=4?s=100" width="100px;" alt="haoww"/><br /><sub><b>haoww</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haoww" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/i-mayuan"><img src="https://avatars.githubusercontent.com/u/101498477?v=4?s=100" width="100px;" alt="i-mayuan"/><br /><sub><b>i-mayuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=i-mayuan" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/fengruge"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="fengruge"/><br /><sub><b>fengruge</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengruge" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/aystzh"><img src="https://avatars.githubusercontent.com/u/38125392?v=4?s=100" width="100px;" alt="zhanghuan"/><br /><sub><b>zhanghuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aystzh" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/shenyumin"><img src="https://avatars.githubusercontent.com/u/8438506?v=4?s=100" width="100px;" alt="shenymin"/><br /><sub><b>shenymin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shenyumin" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/dhruva1995"><img src="https://avatars.githubusercontent.com/u/12976351?v=4?s=100" width="100px;" alt="Dhruva Chandra"/><br /><sub><b>Dhruva Chandra</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dhruva1995" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/weiwang988"><img src="https://avatars.githubusercontent.com/u/58241726?v=4?s=100" width="100px;" alt="miss_z"/><br /><sub><b>miss_z</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weiwang988" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt990"><img src="https://avatars.githubusercontent.com/u/86013697?v=4?s=100" width="100px;" alt="wyt990"/><br /><sub><b>wyt990</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt990" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/licocon"><img src="https://avatars.githubusercontent.com/u/36863277?v=4?s=100" width="100px;" alt="licocon"/><br /><sub><b>licocon</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=licocon" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/2406450951"><img src="https://avatars.githubusercontent.com/u/48074721?v=4?s=100" width="100px;" alt="Mi Na"/><br /><sub><b>Mi Na</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=2406450951" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Kylin-Guo"><img src="https://avatars.githubusercontent.com/u/131239856?v=4?s=100" width="100px;" alt="Kylin-Guo"/><br /><sub><b>Kylin-Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Kylin-Guo" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/1797899698"><img src="https://avatars.githubusercontent.com/u/40411650?v=4?s=100" width="100px;" alt="Mr灬Dong先生"/><br /><sub><b>Mr灬Dong先生</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1797899698" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="http://neilblaze.live"><img src="https://avatars.githubusercontent.com/u/48355572?v=4?s=100" width="100px;" alt="Pratyay Banerjee"/><br /><sub><b>Pratyay Banerjee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yujianzhong520"><img src="https://avatars.githubusercontent.com/u/63705063?v=4?s=100" width="100px;" alt="yujianzhong520"/><br /><sub><b>yujianzhong520</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yujianzhong520" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://sppan24.github.io/"><img src="https://avatars.githubusercontent.com/u/15795173?v=4?s=100" width="100px;" alt="SPPan"/><br /><sub><b>SPPan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sppan24" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/1130600015"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1130600015" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/QZmp236478"><img src="https://avatars.githubusercontent.com/u/56623162?v=4?s=100" width="100px;" alt="impress"/><br /><sub><b>impress</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QZmp236478" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jx3775250"><img src="https://avatars.githubusercontent.com/u/40455946?v=4?s=100" width="100px;" alt="凌晨一点半"/><br /><sub><b>凌晨一点半</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx3775250" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/eeshaanSA"><img src="https://avatars.githubusercontent.com/u/100678386?v=4?s=100" width="100px;" alt="Eeshaan Sawant"/><br /><sub><b>Eeshaan Sawant</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eeshaanSA" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/nandofromthebando"><img src="https://avatars.githubusercontent.com/u/87321214?v=4?s=100" width="100px;" alt="nandofromthebando"/><br /><sub><b>nandofromthebando</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=nandofromthebando" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/caiboking"><img src="https://avatars.githubusercontent.com/u/6509883?v=4?s=100" width="100px;" alt="caiboking"/><br /><sub><b>caiboking</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caiboking" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/baixing99"><img src="https://avatars.githubusercontent.com/u/73473087?v=4?s=100" width="100px;" alt="baixing99"/><br /><sub><b>baixing99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baixing99" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ifrenzyc"><img src="https://avatars.githubusercontent.com/u/543927?v=4?s=100" width="100px;" alt="Yang Chuang"/><br /><sub><b>Yang Chuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ifrenzyc" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wlin20"><img src="https://avatars.githubusercontent.com/u/20657577?v=4?s=100" width="100px;" alt="wlin20"/><br /><sub><b>wlin20</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wlin20" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/guojing1983"><img src="https://avatars.githubusercontent.com/u/60596094?v=4?s=100" width="100px;" alt="guojing1983"/><br /><sub><b>guojing1983</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=guojing1983" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/itxxq"><img src="https://avatars.githubusercontent.com/u/46962357?v=4?s=100" width="100px;" alt="moxi"/><br /><sub><b>moxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=itxxq" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/qq471754603"><img src="https://avatars.githubusercontent.com/u/23146592?v=4?s=100" width="100px;" alt="qq471754603"/><br /><sub><b>qq471754603</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qq471754603" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/san346596324"><img src="https://avatars.githubusercontent.com/u/30828520?v=4?s=100" width="100px;" alt="渭雨"/><br /><sub><b>渭雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=san346596324" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/luoxuanzao"><img src="https://avatars.githubusercontent.com/u/44692579?v=4?s=100" width="100px;" alt="liuxuezhuo"/><br /><sub><b>liuxuezhuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luoxuanzao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ChenXiangxxxxx"><img src="https://avatars.githubusercontent.com/u/90089594?v=4?s=100" width="100px;" alt="ChenXiangxxxxx"/><br /><sub><b>ChenXiangxxxxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChenXiangxxxxx" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Mr-zhou315"><img src="https://avatars.githubusercontent.com/u/10276100?v=4?s=100" width="100px;" alt="Mr.zhou"/><br /><sub><b>Mr.zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Mr-zhou315" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/XimfengYao"><img src="https://avatars.githubusercontent.com/u/17541537?v=4?s=100" width="100px;" alt="姚贤丰"/><br /><sub><b>姚贤丰</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XimfengYao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LINGLUOJUN"><img src="https://avatars.githubusercontent.com/u/16778977?v=4?s=100" width="100px;" alt="lingluojun"/><br /><sub><b>lingluojun</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LINGLUOJUN" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.luelueking.com"><img src="https://avatars.githubusercontent.com/u/93204032?v=4?s=100" width="100px;" alt="1ue"/><br /><sub><b>1ue</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luelueking" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.jimmyqiao.top"><img src="https://avatars.githubusercontent.com/u/67301054?v=4?s=100" width="100px;" alt="qyaaaa"/><br /><sub><b>qyaaaa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qyaaaa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aqyaaaa" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://novohit.top"><img src="https://avatars.githubusercontent.com/u/101090395?v=4?s=100" width="100px;" alt="novohit"/><br /><sub><b>novohit</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=novohit" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/rbsrcy"><img src="https://avatars.githubusercontent.com/u/4798540?v=4?s=100" width="100px;" alt="zhuoshangyi"/><br /><sub><b>zhuoshangyi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=rbsrcy" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ruanliang-hualun"><img src="https://avatars.githubusercontent.com/u/65543716?v=4?s=100" width="100px;" alt="ruanliang"/><br /><sub><b>ruanliang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Eden4701"><img src="https://avatars.githubusercontent.com/u/68422437?v=4?s=100" width="100px;" alt="Eden4701"/><br /><sub><b>Eden4701</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Documentation">📖</a> <a href="#design-Eden4701" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/XiaTian688"><img src="https://avatars.githubusercontent.com/u/111830921?v=4?s=100" width="100px;" alt="XiaTian688"/><br /><sub><b>XiaTian688</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XiaTian688" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/liyin"><img src="https://avatars.githubusercontent.com/u/863169?v=4?s=100" width="100px;" alt="liyinjiang"/><br /><sub><b>liyinjiang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liyin" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jiashu1024"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiashu1024" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ZY945"><img src="https://avatars.githubusercontent.com/u/74083801?v=4?s=100" width="100px;" alt="东风"/><br /><sub><b>东风</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Code">💻</a> <a href="#design-ZY945" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AZY945" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/prolevel1"><img src="https://avatars.githubusercontent.com/u/51995525?v=4?s=100" width="100px;" alt="sonam singh"/><br /><sub><b>sonam singh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=prolevel1" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ZhangZixuan1994"><img src="https://avatars.githubusercontent.com/u/20011653?v=4?s=100" width="100px;" alt="ZhangZixuan1994"/><br /><sub><b>ZhangZixuan1994</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhangZixuan1994" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/hurenjie1"><img src="https://avatars.githubusercontent.com/u/40120355?v=4?s=100" width="100px;" alt="SHIG"/><br /><sub><b>SHIG</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hurenjie1" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://tslj1024.github.io/"><img src="https://avatars.githubusercontent.com/u/155222677?v=4?s=100" width="100px;" alt="泰上老菌"/><br /><sub><b>泰上老菌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tslj1024" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ldysdu"><img src="https://avatars.githubusercontent.com/u/15815338?v=4?s=100" width="100px;" alt="ldysdu"/><br /><sub><b>ldysdu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldysdu" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/GEM0816g"><img src="https://avatars.githubusercontent.com/u/85116017?v=4?s=100" width="100px;" alt="梁同学"/><br /><sub><b>梁同学</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=GEM0816g" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/avvCode"><img src="https://avatars.githubusercontent.com/u/113538532?v=4?s=100" width="100px;" alt="avv"/><br /><sub><b>avv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=avvCode" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yqxxgh"><img src="https://avatars.githubusercontent.com/u/42080876?v=4?s=100" width="100px;" alt="yqxxgh"/><br /><sub><b>yqxxgh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayqxxgh" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieShi46"><img src="https://avatars.githubusercontent.com/u/149798885?v=4?s=100" width="100px;" alt="CharlieShi46"/><br /><sub><b>CharlieShi46</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieShi46" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Nctllnty"><img src="https://avatars.githubusercontent.com/u/33241818?v=4?s=100" width="100px;" alt="Nctllnty"/><br /><sub><b>Nctllnty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Nctllnty" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Wang-Yonghao"><img src="https://avatars.githubusercontent.com/u/48146606?v=4?s=100" width="100px;" alt="Wang-Yonghao"/><br /><sub><b>Wang-Yonghao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Wang-Yonghao" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.yuque.com/dudiao/yy"><img src="https://avatars.githubusercontent.com/u/38355949?v=4?s=100" width="100px;" alt="读钓"/><br /><sub><b>读钓</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dudiao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/starmilkxin"><img src="https://avatars.githubusercontent.com/u/55646681?v=4?s=100" width="100px;" alt="Xin"/><br /><sub><b>Xin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starmilkxin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Astarmilkxin" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/handy-git"><img src="https://avatars.githubusercontent.com/u/32837980?v=4?s=100" width="100px;" alt="handy"/><br /><sub><b>handy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=handy-git" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LiuTianyou"><img src="https://avatars.githubusercontent.com/u/30208283?v=4?s=100" width="100px;" alt="LiuTianyou"/><br /><sub><b>LiuTianyou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALiuTianyou" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Tests">⚠️</a> <a href="#blog-LiuTianyou" title="Blogposts">📝</a> <a href="#design-LiuTianyou" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/WinterKi1ler"><img src="https://avatars.githubusercontent.com/u/160592092?v=4?s=100" width="100px;" alt="WinterKi1ler"/><br /><sub><b>WinterKi1ler</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=WinterKi1ler" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://sharehoo.cn/"><img src="https://avatars.githubusercontent.com/u/45377370?v=4?s=100" width="100px;" alt="miki"/><br /><sub><b>miki</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miki-hmt" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://codeflex.substack.com/"><img src="https://avatars.githubusercontent.com/u/85513042?v=4?s=100" width="100px;" alt="Keshav Carpenter"/><br /><sub><b>Keshav Carpenter</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/makechoicenow"><img src="https://avatars.githubusercontent.com/u/9911918?v=4?s=100" width="100px;" alt="makechoicenow"/><br /><sub><b>makechoicenow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=makechoicenow" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/gjjjj0101"><img src="https://avatars.githubusercontent.com/u/71874373?v=4?s=100" width="100px;" alt="Gao Jian"/><br /><sub><b>Gao Jian</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Documentation">📖</a> <a href="#design-gjjjj0101" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agjjjj0101" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://jangto.tistory.com/"><img src="https://avatars.githubusercontent.com/u/37864182?v=4?s=100" width="100px;" alt="Hyeon Sung"/><br /><sub><b>Hyeon Sung</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://crossoverjie.top/"><img src="https://avatars.githubusercontent.com/u/15684156?v=4?s=100" width="100px;" alt="crossoverJie"/><br /><sub><b>crossoverJie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Documentation">📖</a> <a href="#blog-crossoverJie" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Tests">⚠️</a> <a href="#design-crossoverJie" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/PeixyJ"><img src="https://avatars.githubusercontent.com/u/45998593?v=4?s=100" width="100px;" alt="PeixyJ"/><br /><sub><b>PeixyJ</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PeixyJ" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Hi-Mr-Wind"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="风如歌"/><br /><sub><b>风如歌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Hi-Mr-Wind" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MananPoojara"><img src="https://avatars.githubusercontent.com/u/104253184?v=4?s=100" width="100px;" alt="Manan Pujara"/><br /><sub><b>Manan Pujara</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MananPoojara" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xuziyang"><img src="https://avatars.githubusercontent.com/u/8465969?v=4?s=100" width="100px;" alt="xuziyang"/><br /><sub><b>xuziyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Axuziyang" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lwqzz"><img src="https://avatars.githubusercontent.com/u/62584513?v=4?s=100" width="100px;" alt="lwqzz"/><br /><sub><b>lwqzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwqzz" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/YxYL6125"><img src="https://avatars.githubusercontent.com/u/91076160?v=4?s=100" width="100px;" alt="YxYL"/><br /><sub><b>YxYL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YxYL6125" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/tomorrowshipyltm"><img src="https://avatars.githubusercontent.com/u/61336903?v=4?s=100" width="100px;" alt="tomorrowshipyltm"/><br /><sub><b>tomorrowshipyltm</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomorrowshipyltm" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/15613060203"><img src="https://avatars.githubusercontent.com/u/41351615?v=4?s=100" width="100px;" alt="栗磊"/><br /><sub><b>栗磊</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=15613060203" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Alanxtl"><img src="https://avatars.githubusercontent.com/u/25652981?v=4?s=100" width="100px;" alt="Alan"/><br /><sub><b>Alan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Alanxtl" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.hadoop.wiki/"><img src="https://avatars.githubusercontent.com/u/29418975?v=4?s=100" width="100px;" alt="Jast"/><br /><sub><b>Jast</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Code">💻</a> <a href="#ideas-zhangshenghang" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Documentation">📖</a> <a href="#blog-zhangshenghang" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azhangshenghang" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Tests">⚠️</a> <a href="#design-zhangshenghang" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zuobiao-zhou"><img src="https://avatars.githubusercontent.com/u/61108539?v=4?s=100" width="100px;" alt="Zhang Yuxuan"/><br /><sub><b>Zhang Yuxuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azuobiao-zhou" title="Bug reports">🐛</a> <a href="#blog-zuobiao-zhou" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Pzz-2021"><img src="https://avatars.githubusercontent.com/u/118056735?v=4?s=100" width="100px;" alt="P.P."/><br /><sub><b>P.P.</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Pzz-2021" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LLP2333"><img src="https://avatars.githubusercontent.com/u/61670545?v=4?s=100" width="100px;" alt="llp2333"/><br /><sub><b>llp2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LLP2333" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/HeartLinked"><img src="https://avatars.githubusercontent.com/u/78212101?v=4?s=100" width="100px;" alt="feiyang li"/><br /><sub><b>feiyang li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HeartLinked" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Aias00"><img src="https://avatars.githubusercontent.com/u/25810623?v=4?s=100" width="100px;" alt="aias00"/><br /><sub><b>aias00</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AAias00" title="Bug reports">🐛</a> <a href="#ideas-Aias00" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/westboy"><img src="https://avatars.githubusercontent.com/u/6385565?v=4?s=100" width="100px;" alt="Jin"/><br /><sub><b>Jin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=westboy" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://blog.csdn.net/qq_52397471"><img src="https://avatars.githubusercontent.com/u/77964041?v=4?s=100" width="100px;" alt="YuLuo"/><br /><sub><b>YuLuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayuluo-yx" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Tests">⚠️</a> <a href="#blog-yuluo-yx" title="Blogposts">📝</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Yanshuming1"><img src="https://avatars.githubusercontent.com/u/118667222?v=4?s=100" width="100px;" alt="linDong"/><br /><sub><b>linDong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AYanshuming1" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lwjxy"><img src="https://avatars.githubusercontent.com/u/52726400?v=4?s=100" width="100px;" alt="lwjxy"/><br /><sub><b>lwjxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwjxy" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://thespica.github.io/"><img src="https://avatars.githubusercontent.com/u/119573640?v=4?s=100" width="100px;" alt="John"/><br /><sub><b>John</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/boatrainlsz"><img src="https://avatars.githubusercontent.com/u/18243785?v=4?s=100" width="100px;" alt="boatrainlsz"/><br /><sub><b>boatrainlsz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boatrainlsz" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.yitianyigexiangfa.com/"><img src="https://avatars.githubusercontent.com/u/3973419?v=4?s=100" width="100px;" alt="Bill Lau"/><br /><sub><b>Bill Lau</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaProgrammerLB" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lw-yang"><img src="https://avatars.githubusercontent.com/u/23456873?v=4?s=100" width="100px;" alt="lwyang"/><br /><sub><b>lwyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lw-yang" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xfl12345"><img src="https://avatars.githubusercontent.com/u/17960863?v=4?s=100" width="100px;" alt="xfl12345"/><br /><sub><b>xfl12345</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xfl12345" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yykaue"><img src="https://avatars.githubusercontent.com/u/22905143?v=4?s=100" width="100px;" alt="Limbo"/><br /><sub><b>Limbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yykaue" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/irenhongyan"><img src="https://avatars.githubusercontent.com/u/53438321?v=4?s=100" width="100px;" alt="哈哈哈哈哈哈哈哈哈"/><br /><sub><b>哈哈哈哈哈哈哈哈哈</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=irenhongyan" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ileonli"><img src="https://avatars.githubusercontent.com/u/45332412?v=4?s=100" width="100px;" alt="Leon Li"/><br /><sub><b>Leon Li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ileonli" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://fnil.net/"><img src="https://avatars.githubusercontent.com/u/14142?v=4?s=100" width="100px;" alt="dennis zhuang"/><br /><sub><b>dennis zhuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=killme2008" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/kerwin612"><img src="https://avatars.githubusercontent.com/u/3371163?v=4?s=100" width="100px;" alt="Kerwin Bryant"/><br /><sub><b>Kerwin Bryant</b></sub></a><br /><a href="#design-kerwin612" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akerwin612" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ShineDevelopment"><img src="https://avatars.githubusercontent.com/u/59306780?v=4?s=100" width="100px;" alt="daixianglong"/><br /><sub><b>daixianglong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShineDevelopment" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/mchgood"><img src="https://avatars.githubusercontent.com/u/38482005?v=4?s=100" width="100px;" alt="mchgood"/><br /><sub><b>mchgood</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mchgood" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/pwallk"><img src="https://avatars.githubusercontent.com/u/69385076?v=4?s=100" width="100px;" alt="kangli"/><br /><sub><b>kangli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apwallk" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/cdphantom"><img src="https://avatars.githubusercontent.com/u/12674795?v=4?s=100" width="100px;" alt="cdphantom"/><br /><sub><b>cdphantom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cdphantom" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/asd108908382"><img src="https://avatars.githubusercontent.com/u/77717999?v=4?s=100" width="100px;" alt="jiawei.guo"/><br /><sub><b>jiawei.guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=asd108908382" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/QBH-insist"><img src="https://avatars.githubusercontent.com/u/39401478?v=4?s=100" width="100px;" alt="QBH-insist"/><br /><sub><b>QBH-insist</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QBH-insist" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jiangsh-ui"><img src="https://avatars.githubusercontent.com/u/86990361?v=4?s=100" width="100px;" alt="jiangsh"/><br /><sub><b>jiangsh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiangsh-ui" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/keaifafafa"><img src="https://avatars.githubusercontent.com/u/83876361?v=4?s=100" width="100px;" alt="Keaifa"/><br /><sub><b>Keaifa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=keaifafafa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akeaifafafa" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/loong95"><img src="https://avatars.githubusercontent.com/u/16333958?v=4?s=100" width="100px;" alt="Loong"/><br /><sub><b>Loong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=loong95" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ceekay47"><img src="https://avatars.githubusercontent.com/u/104664857?v=4?s=100" width="100px;" alt="Chandrakant Vankayalapati"/><br /><sub><b>Chandrakant Vankayalapati</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ceekay47" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MRgenial"><img src="https://avatars.githubusercontent.com/u/49973336?v=4?s=100" width="100px;" alt="b_mountain"/><br /><sub><b>b_mountain</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MRgenial" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/TemirlanBasitov"><img src="https://avatars.githubusercontent.com/u/57500808?v=4?s=100" width="100px;" alt="TemirlanBasitov"/><br /><sub><b>TemirlanBasitov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TemirlanBasitov" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wyfvsfy"><img src="https://avatars.githubusercontent.com/u/11973517?v=4?s=100" width="100px;" alt="wyfvsfy"/><br /><sub><b>wyfvsfy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyfvsfy" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/sherry-peng2333"><img src="https://avatars.githubusercontent.com/u/70619577?v=4?s=100" width="100px;" alt="sherry-peng2333"/><br /><sub><b>sherry-peng2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sherry-peng2333" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lixiaobaivv"><img src="https://avatars.githubusercontent.com/u/39290771?v=4?s=100" width="100px;" alt="Yzzz"/><br /><sub><b>Yzzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lixiaobaivv" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.bckf.cn/"><img src="https://avatars.githubusercontent.com/u/13309008?v=4?s=100" width="100px;" alt="puruidong"/><br /><sub><b>puruidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pruidong" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/shinestare"><img src="https://avatars.githubusercontent.com/u/13570619?v=4?s=100" width="100px;" alt="shinestare"/><br /><sub><b>shinestare</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shinestare" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/po-168"><img src="https://avatars.githubusercontent.com/u/185745593?v=4?s=100" width="100px;" alt="po-168"/><br /><sub><b>po-168</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=po-168" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/All-The-Best-for"><img src="https://avatars.githubusercontent.com/u/76414672?v=4?s=100" width="100px;" alt="wbs99"/><br /><sub><b>wbs99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=All-The-Best-for" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/starryCoder"><img src="https://avatars.githubusercontent.com/u/46510059?v=4?s=100" width="100px;" alt="starryCoder"/><br /><sub><b>starryCoder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starryCoder" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/hasimmollah"><img src="https://avatars.githubusercontent.com/u/32538599?v=4?s=100" width="100px;" alt="hasimmollah"/><br /><sub><b>hasimmollah</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hasimmollah" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ayu-v0"><img src="https://avatars.githubusercontent.com/u/127600988?v=4?s=100" width="100px;" alt="Ayu"/><br /><sub><b>Ayu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ayu-v0" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Rancho-7"><img src="https://avatars.githubusercontent.com/u/59016860?v=4?s=100" width="100px;" alt="Nick Guo"/><br /><sub><b>Nick Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ARancho-7" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/doveLin0818"><img src="https://avatars.githubusercontent.com/u/190927907?v=4?s=100" width="100px;" alt="doveLin"/><br /><sub><b>doveLin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=doveLin0818" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://zzrl.cc/"><img src="https://avatars.githubusercontent.com/u/91836599?v=4?s=100" width="100px;" alt="yunfan24"/><br /><sub><b>yunfan24</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayunfan24" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Tests">⚠️</a> <a href="#blog-yunfan24" title="Blogposts">📝</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lctking"><img src="https://avatars.githubusercontent.com/u/168249998?v=4?s=100" width="100px;" alt="nullwli"/><br /><sub><b>nullwli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lctking" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://simonsigre.com/"><img src="https://avatars.githubusercontent.com/u/14932913?v=4?s=100" width="100px;" alt="Simon Sigré"/><br /><sub><b>Simon Sigré</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.ponfee.cn/"><img src="https://avatars.githubusercontent.com/u/46117331?v=4?s=100" width="100px;" alt="ponfee"/><br /><sub><b>ponfee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ponfee" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Vedant7789"><img src="https://avatars.githubusercontent.com/u/147625492?v=4?s=100" width="100px;" alt="Vedant7789"/><br /><sub><b>Vedant7789</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Vedant7789" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Craaaaazy77"><img src="https://avatars.githubusercontent.com/u/23025522?v=4?s=100" width="100px;" alt="Craaaaazy77"/><br /><sub><b>Craaaaazy77</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Craaaaazy77" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Suvrat1629"><img src="https://avatars.githubusercontent.com/u/140749446?v=4?s=100" width="100px;" alt="Suvrat1629"/><br /><sub><b>Suvrat1629</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Suvrat1629" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://ghyghoo8.github.io/"><img src="https://avatars.githubusercontent.com/u/363129?v=4?s=100" width="100px;" alt="ghy"/><br /><sub><b>ghy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ghyghoo8" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/helei1030"><img src="https://avatars.githubusercontent.com/u/11839080?v=4?s=100" width="100px;" alt="helei1030"/><br /><sub><b>helei1030</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=helei1030" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://medium.com/@pjfanning"><img src="https://avatars.githubusercontent.com/u/11783444?v=4?s=100" width="100px;" alt="PJ Fanning"/><br /><sub><b>PJ Fanning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apjfanning" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MonsterChenzhuo"><img src="https://avatars.githubusercontent.com/u/60029759?v=4?s=100" width="100px;" alt="monster"/><br /><sub><b>monster</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MonsterChenzhuo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MasamiYui"><img src="https://avatars.githubusercontent.com/u/22274133?v=4?s=100" width="100px;" alt="Sherlock Yin"/><br /><sub><b>Sherlock Yin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AMasamiYui" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wanhao23"><img src="https://avatars.githubusercontent.com/u/29560961?v=4?s=100" width="100px;" alt="wanhao"/><br /><sub><b>wanhao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jonasHanhan"><img src="https://avatars.githubusercontent.com/u/130035609?v=4?s=100" width="100px;" alt="jonasHanhan"/><br /><sub><b>jonasHanhan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jonasHanhan" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/NikhilMurugesan"><img src="https://avatars.githubusercontent.com/u/49281792?v=4?s=100" width="100px;" alt="NikhilMurugesan"/><br /><sub><b>NikhilMurugesan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NikhilMurugesan" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/myangle1120"><img src="https://avatars.githubusercontent.com/u/19237013?v=4?s=100" width="100px;" alt="myangle1120"/><br /><sub><b>myangle1120</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=myangle1120" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Anotbugggg" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Documentation">📖</a> <a href="#ideas-JuJinPark" title="Ideas, Planning, & Feedback">🤔</a> <a href="#blog-JuJinPark" title="Blogposts">📝</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALL-LIN" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abigcyy" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td> + </tr> + </tbody> +</table> + +<!-- markdownlint-restore --> +<!-- prettier-ignore-end --> + +<!-- ALL-CONTRIBUTORS-LIST:END --> + +## 💬 Join discussion + +##### Channel + +[Join the Mailing Lists](https://lists.apache.org/list.html?dev@hertzbeat.apache.org) : Mail to `dev-subscribe@hertzbeat.apache.org` to subscribe mailing lists. + +[Chat On Discord](https://discord.gg/Fb6M73htGr) + +WeChat Group : Add friend `ahertzbeat` and invite to the group. + +WeChat Official Account : Search ID `usthecom`. + +[Github Discussion](https://github.com/apache/hertzbeat/discussions) + +[Follow Us Twitter](https://x.com/hertzbeat1024) + +[Subscribe YouTube](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow) + +##### Open-Source Project Build From Open-Source + +HertzBeat is built on so many great open source projects, thanks to them! + +- `Java Spring SpringBoot Jpa Maven Assembly Netty Lombok Sureness Protobuf HttpClient Guava SnakeYaml JsonPath ...` +- `TypeScript Angular NG-ZORRO NG-ALAIN NodeJs Npm Html Less Echarts Rxjs ZoneJs MonacoEditor SlickCarousel Docusaurus ...` + +## Landscape + +<p align="left"> +<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300">  <img src="./home/static/img/home/cncf-right-logo.svg" width="345" /> +<br /><br /> +HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat"> +CNCF Observability And Analysis - Observability Landscape.</a> +</p> + +## 🛡️ License +[`Apache License, Version 2.0`](https://www.apache.org/licenses/LICENSE-2.0.html) diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..e05498e --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`apache/hertzbeat` +- 原始仓库:https://github.com/apache/hertzbeat +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..b445a1a --- /dev/null +++ b/README_CN.md @@ -0,0 +1,597 @@ +<p align="center"> + <a href="https://hertzbeat.apache.org"> + <img alt="hertzbeat" src="/home/static/img/hertzbeat-brand.svg" width="260"> + </a> +</p> + +<p align="center"> +<b>Readme</b>: +<a href="README.md">English</a> | <b><a href="README_CN.md">中文</a></b> | <a href="README_JP.md">日本語</a> +</p> + +[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr) +[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://x.com/hertzbeat1024) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8139/badge)](https://www.bestpractices.dev/projects/8139) +[![codecov](https://codecov.io/gh/apache/HertzBeat/branch/master/graph/badge.svg)](https://app.codecov.io/gh/apache/hertzbeat) +[![Docker Pulls](https://img.shields.io/docker/pulls/apache/hertzbeat?style=%20for-the-badge&logo=docker&label=DockerHub%20Download)](https://hub.docker.com/r/apache/hertzbeat) +[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/hertzbeat)](https://artifacthub.io/packages/search?repo=hertzbeat) +[![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCri75zfWX0GHqJFPENEbLow?logo=youtube&label=YouTube%20Channel)](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow) +[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/apache/hertzbeat) + + +**官网: [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,高性能集群,提供强大的自定义监控和状态页构建能力。 + +### 特点 + +- 集```采集+分析+告警+通知```为一体,HertzBeat AI 驱动下的新交互与功能,也内置 MCP Server 对外能力。 +- 统一的指标平台,无需 Agent,兼容 Prometheus,支持应用服务,程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等。 +- 统一的日志平台,通过 OTLP 协议多日志源无缝对接上报。 +- 统一的告警平台,内部告警与外部多种告警源集成接入,统一告警处理分析,灵活的实时与周期阈值规则,分组收敛,静默,抑制等。 +- 统一的消息分发,告警平台处理后通过 `邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式分发通知。 +- 将 `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需配置模板 `YML` 就能自定义采集指标。您相信只需简单配置即可快速适配一款 `K8s` 或 `Docker` 等新的监控类型吗? +- 高性能,支持多采集器集群横向扩展,支持多隔离网络监控,云边协同。 +- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。 + + +> `HertzBeat`的统一平台,AI智能,强大自定义,多类型支持,高性能,易扩展,希望能帮助用户快速方便实现观测需求。 + +---- + +---- + +## 🥐 模块 + +![hertzBeat](home/static/img/docs/hertzbeat-arch.png) + +## ⛄ 已支持 + +> 我们将监控采集类型(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 支持通过源码安装启动,Docker 容器运行和安装包方式安装部署,CPU 架构支持 x86/arm64。 + +##### 方式一:Docker 方式快速安装 + +1. `docker` 环境仅需一条命令即可开始 + + ```shell + docker run -d -p 1157:1157 -p 1158:1158 --name hertzbeat apache/hertzbeat + ``` + +2. 浏览器访问 `http://localhost:1157` 即可开始,默认账号密码 `admin/hertzbeat` + +3. 部署采集器集群(可选) + + ```shell + docker run -d -e IDENTITY=custom-collector-name -e MANAGER_HOST=127.0.0.1 -e MANAGER_PORT=1158 --name hertzbeat-collector apache/hertzbeat-collector + ``` + + - `-e IDENTITY=custom-collector-name` : 配置此采集器的唯一性标识符名称,多个采集器名称不能相同,建议自定义英文名称。 + - `-e MODE=public` : 配置运行模式(public or private), 公共集群模式或私有云边模式。 + - `-e MANAGER_HOST=127.0.0.1` : 配置连接主 HertzBeat 服务的对外 IP。 + - `-e MANAGER_PORT=1158` : 配置连接主 HertzBeat 服务的对外端口,默认1158。 + + +更多配置详细步骤参考 [通过 Docker 方式安装 HertzBeat](https://hertzbeat.apache.org/docs/start/docker-deploy) + +##### 方式二:通过安装包安装 + +1. 下载您系统环境对应的安装包 `apache-hertzbeat-xx-bin.tar.gz` [Download](https://hertzbeat.apache.org/docs/download) +2. 配置 HertzBeat 的配置文件 `hertzbeat/config/application.yml` (可选) +3. 部署启动 `$ ./bin/startup.sh ` 或 `bin/startup.bat` +4. 浏览器访问 `http://localhost:1157` 即可开始,默认账号密码 `admin/hertzbeat` +5. 部署采集器集群(可选) + - 下载采集器安装包 `apache-hertzbeat-collector-xx-bin.tar.gz`(JVM 采集器)或与你目标平台匹配的 Native 采集器安装包,例如 `apache-hertzbeat-collector-native-xx-linux-amd64-bin.tar.gz`、`apache-hertzbeat-collector-native-xx-windows-amd64-bin.zip`,到规划的另一台部署主机上 [Download](https://hertzbeat.apache.org/docs/download) + - 配置采集器的配置文件 `hertzbeat-collector/config/application.yml` 里面的连接主 HertzBeat 服务的对外 IP,端口,当前采集器名称(需保证唯一性)等参数 `identity` `mode` (public or private) `manager-host` `manager-port` + ```yaml + collector: + dispatch: + entrance: + netty: + enabled: true + identity: ${IDENTITY:} + mode: ${MODE:public} + manager-host: ${MANAGER_HOST:127.0.0.1} + manager-port: ${MANAGER_PORT:1158} + ``` + - 如果没有在 `ext-lib` 中提供 JDBC 驱动,MySQL、MariaDB、OceanBase 可以直接使用内置查询引擎,也可以使用 Native 采集器安装包;TiDB 的 SQL 查询指标也遵循同样规则。 + - 如果在 `ext-lib` 中放入了 `mysql-connector-j`,主程序内置采集器或 JVM 采集器会在重启后自动优先走 JDBC;这一点现在适用于 MySQL、MariaDB、OceanBase,TiDB 的 SQL 查询指标也遵循同样规则,而它的 HTTP 指标不受影响。Oracle、DB2 仍然必须使用 JVM 采集器安装包,因为它们依赖外置 JDBC 驱动。 + - JVM 采集器安装包使用 `$ ./bin/startup.sh ` 或 `bin/startup.bat` 启动。Linux 或 macOS 的 Native 采集器安装包使用 `$ ./bin/startup.sh ` 启动,Windows 的 Native 采集器安装包使用 `bin\\startup.bat` 启动 + - 浏览器访问主 HertzBeat 服务 `http://localhost:1157` 查看概览页面即可看到注册上来的新采集器 + +更多配置详细步骤参考 [通过安装包安装HertzBeat](https://hertzbeat.apache.org/docs/start/package-deploy) + +##### 方式三:本地代码启动 + +1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `hertzbeat-startup` 和前端工程 `web-app` +2. 后端:需要 `maven3+`, `java25` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `hertzbeat-startup` 服务即可。 +3. 前端:需要 `nodejs` 和 `pnpm` 环境,待本地后端启动后,在 `web-app` 目录下执行 `pnpm install` 再执行 `pnpm start` +4. 浏览器访问 `http://localhost:4200` 即可开始,默认账号密码 `admin/hertzbeat` + +详细步骤参考 [参与贡献之本地代码启动](CONTRIBUTING.md) + +##### 方式四:Docker-Compose 统一安装 hertzbeat+postgresql+tsdb + +通过 [Docker-Compose 部署脚本](script/docker-compose) 一次性把 postgresql/mysql 数据库, victoria-metrics/iotdb/tdengine 时序数据库和 hertzbeat 安装部署。 + +详细步骤参考 [通过 Docker-Compose 安装 HertzBeat](script/docker-compose/README.md) + +##### 方式五:Kubernetes Helm Charts 部署 hertzbeat+collector+postgresql+tsdb + +通过 Helm Chart 一次性将 HertzBeat 集群组件部署到 Kubernetes 集群中。 + +详细步骤参考 [Artifact Hub](https://artifacthub.io/packages/helm/hertzbeat/hertzbeat) + +**HAVE FUN** + +## ✨ Contributors + +Thanks these wonderful people, welcome to join us: +[贡献者指南](CONTRIBUTING.md) + +<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> +<!-- prettier-ignore-start --> +<!-- markdownlint-disable --> +<table> + <tbody> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/tomsun28"><img src="https://avatars.githubusercontent.com/u/24788200?v=4?s=100" width="100px;" alt="tomsun28"/><br /><sub><b>tomsun28</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Documentation">📖</a> <a href="#design-tomsun28" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wang1027-wqh"><img src="https://avatars.githubusercontent.com/u/71161318?v=4?s=100" width="100px;" alt="会编程的王学长"/><br /><sub><b>会编程的王学长</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Documentation">📖</a> <a href="#design-wang1027-wqh" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.maxkey.top/"><img src="https://avatars.githubusercontent.com/u/1563377?v=4?s=100" width="100px;" alt="MaxKey"/><br /><sub><b>MaxKey</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shimingxy" title="Code">💻</a> <a href="#design-shimingxy" title="Design">🎨</a> <a href="#ideas-shimingxy" title="Ideas, Planning, & Feedback">🤔</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://blog.gcdd.top/"><img src="https://avatars.githubusercontent.com/u/26523525?v=4?s=100" width="100px;" alt="观沧海"/><br /><sub><b>观沧海</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gcdd1993" title="Code">💻</a> <a href="#design-gcdd1993" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agcdd1993" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/a25017012"><img src="https://avatars.githubusercontent.com/u/32265356?v=4?s=100" width="100px;" alt="yuye"/><br /><sub><b>yuye</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jx10086"><img src="https://avatars.githubusercontent.com/u/5323228?v=4?s=100" width="100px;" alt="jx10086"/><br /><sub><b>jx10086</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx10086" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ajx10086" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/winnerTimer"><img src="https://avatars.githubusercontent.com/u/76024658?v=4?s=100" width="100px;" alt="winnerTimer"/><br /><sub><b>winnerTimer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=winnerTimer" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AwinnerTimer" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/goo-kits"><img src="https://avatars.githubusercontent.com/u/13163673?v=4?s=100" width="100px;" alt="goo-kits"/><br /><sub><b>goo-kits</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=goo-kits" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agoo-kits" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/brave4Time"><img src="https://avatars.githubusercontent.com/u/105094014?v=4?s=100" width="100px;" alt="brave4Time"/><br /><sub><b>brave4Time</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brave4Time" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abrave4Time" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/walkerlee-lab"><img src="https://avatars.githubusercontent.com/u/8426753?v=4?s=100" width="100px;" alt="WalkerLee"/><br /><sub><b>WalkerLee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=walkerlee-lab" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Awalkerlee-lab" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/fullofjoy"><img src="https://avatars.githubusercontent.com/u/30247571?v=4?s=100" width="100px;" alt="jianghang"/><br /><sub><b>jianghang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fullofjoy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Afullofjoy" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ChineseTony"><img src="https://avatars.githubusercontent.com/u/24618786?v=4?s=100" width="100px;" alt="ChineseTony"/><br /><sub><b>ChineseTony</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChineseTony" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AChineseTony" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt199905"><img src="https://avatars.githubusercontent.com/u/85098809?v=4?s=100" width="100px;" alt="wyt199905"/><br /><sub><b>wyt199905</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt199905" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/weifuqing"><img src="https://avatars.githubusercontent.com/u/13931013?v=4?s=100" width="100px;" alt="卫傅庆"/><br /><sub><b>卫傅庆</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weifuqing" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aweifuqing" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zklmcookle"><img src="https://avatars.githubusercontent.com/u/107192352?v=4?s=100" width="100px;" alt="zklmcookle"/><br /><sub><b>zklmcookle</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zklmcookle" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/DevilX5"><img src="https://avatars.githubusercontent.com/u/13269921?v=4?s=100" width="100px;" alt="DevilX5"/><br /><sub><b>DevilX5</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/djzeng"><img src="https://avatars.githubusercontent.com/u/14074864?v=4?s=100" width="100px;" alt="tea"/><br /><sub><b>tea</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=djzeng" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yangshihui"><img src="https://avatars.githubusercontent.com/u/28550208?v=4?s=100" width="100px;" alt="yangshihui"/><br /><sub><b>yangshihui</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yangshihui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayangshihui" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/DreamGirl524"><img src="https://avatars.githubusercontent.com/u/81132838?v=4?s=100" width="100px;" alt="DreamGirl524"/><br /><sub><b>DreamGirl524</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/gzwlly"><img src="https://avatars.githubusercontent.com/u/83171907?v=4?s=100" width="100px;" alt="gzwlly"/><br /><sub><b>gzwlly</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gzwlly" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/cuipiheqiuqiu"><img src="https://avatars.githubusercontent.com/u/76642201?v=4?s=100" width="100px;" alt="cuipiheqiuqiu"/><br /><sub><b>cuipiheqiuqiu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Tests">⚠️</a> <a href="#design-cuipiheqiuqiu" title="Design">🎨</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/oyiyou"><img src="https://avatars.githubusercontent.com/u/39228891?v=4?s=100" width="100px;" alt="lambert"/><br /><sub><b>lambert</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=oyiyou" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://mroldx.xyz/"><img src="https://avatars.githubusercontent.com/u/34847828?v=4?s=100" width="100px;" alt="mroldx"/><br /><sub><b>mroldx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mroldx" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/woshiniusange"><img src="https://avatars.githubusercontent.com/u/91513022?v=4?s=100" width="100px;" alt="woshiniusange"/><br /><sub><b>woshiniusange</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=woshiniusange" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://vampireachao.github.io/"><img src="https://avatars.githubusercontent.com/u/52746628?v=4?s=100" width="100px;" alt="VampireAchao"/><br /><sub><b>VampireAchao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=VampireAchao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Ceilzcx"><img src="https://avatars.githubusercontent.com/u/48920254?v=4?s=100" width="100px;" alt="zcx"/><br /><sub><b>zcx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACeilzcx" title="Bug reports">🐛</a> <a href="#design-Ceilzcx" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Tests">⚠️</a> <a href="#blog-Ceilzcx" title="Blogposts">📝</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieXCL"><img src="https://avatars.githubusercontent.com/u/91540487?v=4?s=100" width="100px;" alt="CharlieXCL"/><br /><sub><b>CharlieXCL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieXCL" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Privauto"><img src="https://avatars.githubusercontent.com/u/36581456?v=4?s=100" width="100px;" alt="Privauto"/><br /><sub><b>Privauto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/emrys-he"><img src="https://avatars.githubusercontent.com/u/5848915?v=4?s=100" width="100px;" alt="emrys"/><br /><sub><b>emrys</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=emrys-he" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/SxLiuYu"><img src="https://avatars.githubusercontent.com/u/95198625?v=4?s=100" width="100px;" alt="SxLiuYu"/><br /><sub><b>SxLiuYu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ASxLiuYu" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://allcontributors.org"><img src="https://avatars.githubusercontent.com/u/46410174?v=4?s=100" width="100px;" alt="All Contributors"/><br /><sub><b>All Contributors</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=all-contributors" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/gxc-myh"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/click33"><img src="https://avatars.githubusercontent.com/u/36243476?v=4?s=100" width="100px;" alt="click33"/><br /><sub><b>click33</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=click33" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://jpom.io/"><img src="https://avatars.githubusercontent.com/u/16408873?v=4?s=100" width="100px;" alt="蒋小小"/><br /><sub><b>蒋小小</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bwcx-jzy" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.zhihu.com/people/kevinbauer"><img src="https://avatars.githubusercontent.com/u/28581579?v=4?s=100" width="100px;" alt="Kevin Huang"/><br /><sub><b>Kevin Huang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=kevinhuangwl" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/TJxiaobao"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ATJxiaobao" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Tests">⚠️</a> <a href="#design-TJxiaobao" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Jack-123-power"><img src="https://avatars.githubusercontent.com/u/84333501?v=4?s=100" width="100px;" alt="Captain Jack"/><br /><sub><b>Captain Jack</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jack-123-power" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/haibo-duan"><img src="https://avatars.githubusercontent.com/u/7974845?v=4?s=100" width="100px;" alt="haibo.duan"/><br /><sub><b>haibo.duan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/assassinfym"><img src="https://avatars.githubusercontent.com/u/15188754?v=4?s=100" width="100px;" alt="assassin"/><br /><sub><b>assassin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Aassassinfym" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=assassinfym" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/csyshu"><img src="https://avatars.githubusercontent.com/u/46591658?v=4?s=100" width="100px;" alt="Reverse wind"/><br /><sub><b>Reverse wind</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/luxx-lq"><img src="https://avatars.githubusercontent.com/u/58515565?v=4?s=100" width="100px;" alt="luxx"/><br /><sub><b>luxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luxx-lq" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eltociear" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zenan08"><img src="https://avatars.githubusercontent.com/u/80514991?v=4?s=100" width="100px;" alt="leizenan"/><br /><sub><b>leizenan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zenan08" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/BKing2020"><img src="https://avatars.githubusercontent.com/u/28869121?v=4?s=100" width="100px;" alt="BKing"/><br /><sub><b>BKing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BKing2020" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xingshuaiLi"><img src="https://avatars.githubusercontent.com/u/119487588?v=4?s=100" width="100px;" alt="xingshuaiLi"/><br /><sub><b>xingshuaiLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xingshuaiLi" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wangke6666"><img src="https://avatars.githubusercontent.com/u/113656595?v=4?s=100" width="100px;" alt="wangke6666"/><br /><sub><b>wangke6666</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wangke6666" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LWBobo"><img src="https://avatars.githubusercontent.com/u/50368698?v=4?s=100" width="100px;" alt="刺猬"/><br /><sub><b>刺猬</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ALWBobo" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LWBobo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.zanglikun.com"><img src="https://avatars.githubusercontent.com/u/61591648?v=4?s=100" width="100px;" alt="Haste"/><br /><sub><b>Haste</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zanglikun" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/SuitSmile"><img src="https://avatars.githubusercontent.com/u/38679717?v=4?s=100" width="100px;" alt="zhongshi.yi"/><br /><sub><b>zhongshi.yi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SuitSmile" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://www.smallq.cn"><img src="https://avatars.githubusercontent.com/u/39754275?v=4?s=100" width="100px;" alt="Qi Zhang"/><br /><sub><b>Qi Zhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zzzhangqi" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MrAndyMing"><img src="https://avatars.githubusercontent.com/u/49541483?v=4?s=100" width="100px;" alt="MrAndyMing"/><br /><sub><b>MrAndyMing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrAndyMing" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://idongliming.github.io/"><img src="https://avatars.githubusercontent.com/u/31564353?v=4?s=100" width="100px;" alt="idongliming"/><br /><sub><b>idongliming</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=idongliming" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://earthjasonlin.github.io"><img src="https://avatars.githubusercontent.com/u/83632110?v=4?s=100" width="100px;" alt="Zichao Lin"/><br /><sub><b>Zichao Lin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://blog.liudonghua.com"><img src="https://avatars.githubusercontent.com/u/2276718?v=4?s=100" width="100px;" alt="liudonghua"/><br /><sub><b>liudonghua</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liudonghua123" title="Code">💻</a> <a href="#ideas-liudonghua123" title="Ideas, Planning, & Feedback">🤔</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeyts"><img src="https://avatars.githubusercontent.com/u/4250869?v=4?s=100" width="100px;" alt="Jerry"/><br /><sub><b>Jerry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Tests">⚠️</a> <a href="#ideas-orangeyts" title="Ideas, Planning, & Feedback">🤔</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://dynamictp.cn"><img src="https://avatars.githubusercontent.com/u/13051908?v=4?s=100" width="100px;" alt="yanhom"/><br /><sub><b>yanhom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yanhom1314" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://www.jianshu.com/u/a8f822c04f67"><img src="https://avatars.githubusercontent.com/u/18587688?v=4?s=100" width="100px;" alt="fsl"/><br /><sub><b>fsl</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengshunli" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xttttv"><img src="https://avatars.githubusercontent.com/u/116323904?v=4?s=100" width="100px;" alt="xttttv"/><br /><sub><b>xttttv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xttttv" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/NavinKumarBarnwal"><img src="https://avatars.githubusercontent.com/u/44504274?v=4?s=100" width="100px;" alt="NavinKumarBarnwal"/><br /><sub><b>NavinKumarBarnwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NavinKumarBarnwal" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/z641205699"><img src="https://avatars.githubusercontent.com/u/45276423?v=4?s=100" width="100px;" alt="Zakkary"/><br /><sub><b>Zakkary</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=z641205699" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/898349230"><img src="https://avatars.githubusercontent.com/u/21972532?v=4?s=100" width="100px;" alt="sunxinbo"/><br /><sub><b>sunxinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ldzbook"><img src="https://avatars.githubusercontent.com/u/13903790?v=4?s=100" width="100px;" alt="ldzbook"/><br /><sub><b>ldzbook</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldzbook" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aldzbook" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/SurryChen"><img src="https://avatars.githubusercontent.com/u/91116490?v=4?s=100" width="100px;" alt="余与雨"/><br /><sub><b>余与雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Tests">⚠️</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MysticalDream"><img src="https://avatars.githubusercontent.com/u/78899028?v=4?s=100" width="100px;" alt="MysticalDream"/><br /><sub><b>MysticalDream</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zhouyoulin12"><img src="https://avatars.githubusercontent.com/u/17086633?v=4?s=100" width="100px;" alt="zhouyoulin12"/><br /><sub><b>zhouyoulin12</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jerjjj"><img src="https://avatars.githubusercontent.com/u/93431283?v=4?s=100" width="100px;" alt="jerjjj"/><br /><sub><b>jerjjj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jerjjj" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://wjl110.xyz/"><img src="https://avatars.githubusercontent.com/u/53851034?v=4?s=100" width="100px;" alt="wjl110"/><br /><sub><b>wjl110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wjl110" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ngyhd"><img src="https://avatars.githubusercontent.com/u/29095207?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ngyhd" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Daydreamer-ia"><img src="https://avatars.githubusercontent.com/u/83362909?v=4?s=100" width="100px;" alt="chenyiqin"/><br /><sub><b>chenyiqin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/hudongdong129"><img src="https://avatars.githubusercontent.com/u/34374227?v=4?s=100" width="100px;" alt="hudongdong129"/><br /><sub><b>hudongdong129</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Documentation">📖</a> <a href="#design-hudongdong129" title="Design">🎨</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/TherChenYang"><img src="https://avatars.githubusercontent.com/u/124348939?v=4?s=100" width="100px;" alt="TherChenYang"/><br /><sub><b>TherChenYang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/HattoriHenzo"><img src="https://avatars.githubusercontent.com/u/5141285?v=4?s=100" width="100px;" alt="HattoriHenzo"/><br /><sub><b>HattoriHenzo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ycilry"><img src="https://avatars.githubusercontent.com/u/63967101?v=4?s=100" width="100px;" alt="ycilry"/><br /><sub><b>ycilry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ycilry" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/aoshiguchen"><img src="https://avatars.githubusercontent.com/u/10580997?v=4?s=100" width="100px;" alt="aoshiguchen"/><br /><sub><b>aoshiguchen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/caibenxiang"><img src="https://avatars.githubusercontent.com/u/4568241?v=4?s=100" width="100px;" alt="蔡本祥"/><br /><sub><b>蔡本祥</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caibenxiang" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.fckeverything.cn:4000/"><img src="https://avatars.githubusercontent.com/u/13827124?v=4?s=100" width="100px;" alt="浮游"/><br /><sub><b>浮游</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lifefloating" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Grass-Life"><img src="https://avatars.githubusercontent.com/u/114381513?v=4?s=100" width="100px;" alt="Grass-Life"/><br /><sub><b>Grass-Life</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Grass-Life" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaohe428"><img src="https://avatars.githubusercontent.com/u/99130317?v=4?s=100" width="100px;" alt="xiaohe428"/><br /><sub><b>xiaohe428</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/baiban114"><img src="https://avatars.githubusercontent.com/u/59152619?v=4?s=100" width="100px;" alt="TableRow"/><br /><sub><b>TableRow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ByteIDance"><img src="https://avatars.githubusercontent.com/u/100207562?v=4?s=100" width="100px;" alt="ByteIDance"/><br /><sub><b>ByteIDance</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ByteIDance" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/mangel2002"><img src="https://avatars.githubusercontent.com/u/9348020?v=4?s=100" width="100px;" alt="Jangfe"/><br /><sub><b>Jangfe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mangel2002" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zqr10159"><img src="https://avatars.githubusercontent.com/u/30048352?v=4?s=100" width="100px;" alt="zqr10159"/><br /><sub><b>zqr10159</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Code">💻</a> <a href="#blog-zqr10159" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azqr10159" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Tests">⚠️</a> <a href="#design-zqr10159" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/vinci-897"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="vinci"/><br /><sub><b>vinci</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Documentation">📖</a> <a href="#design-vinci-897" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/js110"><img src="https://avatars.githubusercontent.com/u/51191863?v=4?s=100" width="100px;" alt="js110"/><br /><sub><b>js110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=js110" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/JavaLionLi"><img src="https://avatars.githubusercontent.com/u/31852897?v=4?s=100" width="100px;" alt="CrazyLionLi"/><br /><sub><b>CrazyLionLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaLionLi" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.banmajio.com"><img src="https://avatars.githubusercontent.com/u/53471385?v=4?s=100" width="100px;" alt="banmajio"/><br /><sub><b>banmajio</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=banmajio" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://suder.fun"><img src="https://avatars.githubusercontent.com/u/69955165?v=4?s=100" width="100px;" alt="topsuder"/><br /><sub><b>topsuder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=topsuder" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/richar2022"><img src="https://avatars.githubusercontent.com/u/129016397?v=4?s=100" width="100px;" alt="richar2022"/><br /><sub><b>richar2022</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=richar2022" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/fcb-xiaobo"><img src="https://avatars.githubusercontent.com/u/60566194?v=4?s=100" width="100px;" alt="fcb-xiaobo"/><br /><sub><b>fcb-xiaobo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fcb-xiaobo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wenkyzhang"><img src="https://avatars.githubusercontent.com/u/13983669?v=4?s=100" width="100px;" alt="wenkyzhang"/><br /><sub><b>wenkyzhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wenkyzhang" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ZangJuxy"><img src="https://avatars.githubusercontent.com/u/71380295?v=4?s=100" width="100px;" alt="ZangJuxy"/><br /><sub><b>ZangJuxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZangJuxy" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/l646505418"><img src="https://avatars.githubusercontent.com/u/50475131?v=4?s=100" width="100px;" alt="l646505418"/><br /><sub><b>l646505418</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=l646505418" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Al646505418" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.carpewang.com"><img src="https://avatars.githubusercontent.com/u/78642589?v=4?s=100" width="100px;" alt="Carpe-Wang"/><br /><sub><b>Carpe-Wang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Carpe-Wang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACarpe-Wang" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/moshu023"><img src="https://avatars.githubusercontent.com/u/48593205?v=4?s=100" width="100px;" alt="莫枢"/><br /><sub><b>莫枢</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moshu023" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/huangcanda"><img src="https://avatars.githubusercontent.com/u/4470566?v=4?s=100" width="100px;" alt="huangcanda"/><br /><sub><b>huangcanda</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=huangcanda" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.zrkizzy.com"><img src="https://avatars.githubusercontent.com/u/85340613?v=4?s=100" width="100px;" alt="世纪末的架构师"/><br /><sub><b>世纪末的架构师</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Architect-Java" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ShuningWan"><img src="https://avatars.githubusercontent.com/u/31086770?v=4?s=100" width="100px;" alt="ShuningWan"/><br /><sub><b>ShuningWan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShuningWan" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MrYZhou"><img src="https://avatars.githubusercontent.com/u/44339602?v=4?s=100" width="100px;" alt="MrYZhou"/><br /><sub><b>MrYZhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrYZhou" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/suncqujsj"><img src="https://avatars.githubusercontent.com/u/8012932?v=4?s=100" width="100px;" alt="suncqujsj"/><br /><sub><b>suncqujsj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=suncqujsj" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/sunqinbo"><img src="https://avatars.githubusercontent.com/u/1428540?v=4?s=100" width="100px;" alt="sunqinbo"/><br /><sub><b>sunqinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sunqinbo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/haoww"><img src="https://avatars.githubusercontent.com/u/32739294?v=4?s=100" width="100px;" alt="haoww"/><br /><sub><b>haoww</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haoww" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/i-mayuan"><img src="https://avatars.githubusercontent.com/u/101498477?v=4?s=100" width="100px;" alt="i-mayuan"/><br /><sub><b>i-mayuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=i-mayuan" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/fengruge"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="fengruge"/><br /><sub><b>fengruge</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengruge" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/aystzh"><img src="https://avatars.githubusercontent.com/u/38125392?v=4?s=100" width="100px;" alt="zhanghuan"/><br /><sub><b>zhanghuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aystzh" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/shenyumin"><img src="https://avatars.githubusercontent.com/u/8438506?v=4?s=100" width="100px;" alt="shenymin"/><br /><sub><b>shenymin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shenyumin" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/dhruva1995"><img src="https://avatars.githubusercontent.com/u/12976351?v=4?s=100" width="100px;" alt="Dhruva Chandra"/><br /><sub><b>Dhruva Chandra</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dhruva1995" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/weiwang988"><img src="https://avatars.githubusercontent.com/u/58241726?v=4?s=100" width="100px;" alt="miss_z"/><br /><sub><b>miss_z</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weiwang988" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt990"><img src="https://avatars.githubusercontent.com/u/86013697?v=4?s=100" width="100px;" alt="wyt990"/><br /><sub><b>wyt990</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt990" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/licocon"><img src="https://avatars.githubusercontent.com/u/36863277?v=4?s=100" width="100px;" alt="licocon"/><br /><sub><b>licocon</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=licocon" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/2406450951"><img src="https://avatars.githubusercontent.com/u/48074721?v=4?s=100" width="100px;" alt="Mi Na"/><br /><sub><b>Mi Na</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=2406450951" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Kylin-Guo"><img src="https://avatars.githubusercontent.com/u/131239856?v=4?s=100" width="100px;" alt="Kylin-Guo"/><br /><sub><b>Kylin-Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Kylin-Guo" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/1797899698"><img src="https://avatars.githubusercontent.com/u/40411650?v=4?s=100" width="100px;" alt="Mr灬Dong先生"/><br /><sub><b>Mr灬Dong先生</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1797899698" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="http://neilblaze.live"><img src="https://avatars.githubusercontent.com/u/48355572?v=4?s=100" width="100px;" alt="Pratyay Banerjee"/><br /><sub><b>Pratyay Banerjee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yujianzhong520"><img src="https://avatars.githubusercontent.com/u/63705063?v=4?s=100" width="100px;" alt="yujianzhong520"/><br /><sub><b>yujianzhong520</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yujianzhong520" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://sppan24.github.io/"><img src="https://avatars.githubusercontent.com/u/15795173?v=4?s=100" width="100px;" alt="SPPan"/><br /><sub><b>SPPan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sppan24" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/1130600015"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1130600015" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/QZmp236478"><img src="https://avatars.githubusercontent.com/u/56623162?v=4?s=100" width="100px;" alt="impress"/><br /><sub><b>impress</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QZmp236478" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jx3775250"><img src="https://avatars.githubusercontent.com/u/40455946?v=4?s=100" width="100px;" alt="凌晨一点半"/><br /><sub><b>凌晨一点半</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx3775250" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/eeshaanSA"><img src="https://avatars.githubusercontent.com/u/100678386?v=4?s=100" width="100px;" alt="Eeshaan Sawant"/><br /><sub><b>Eeshaan Sawant</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eeshaanSA" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/nandofromthebando"><img src="https://avatars.githubusercontent.com/u/87321214?v=4?s=100" width="100px;" alt="nandofromthebando"/><br /><sub><b>nandofromthebando</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=nandofromthebando" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/caiboking"><img src="https://avatars.githubusercontent.com/u/6509883?v=4?s=100" width="100px;" alt="caiboking"/><br /><sub><b>caiboking</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caiboking" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/baixing99"><img src="https://avatars.githubusercontent.com/u/73473087?v=4?s=100" width="100px;" alt="baixing99"/><br /><sub><b>baixing99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baixing99" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ifrenzyc"><img src="https://avatars.githubusercontent.com/u/543927?v=4?s=100" width="100px;" alt="Yang Chuang"/><br /><sub><b>Yang Chuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ifrenzyc" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wlin20"><img src="https://avatars.githubusercontent.com/u/20657577?v=4?s=100" width="100px;" alt="wlin20"/><br /><sub><b>wlin20</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wlin20" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/guojing1983"><img src="https://avatars.githubusercontent.com/u/60596094?v=4?s=100" width="100px;" alt="guojing1983"/><br /><sub><b>guojing1983</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=guojing1983" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/itxxq"><img src="https://avatars.githubusercontent.com/u/46962357?v=4?s=100" width="100px;" alt="moxi"/><br /><sub><b>moxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=itxxq" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/qq471754603"><img src="https://avatars.githubusercontent.com/u/23146592?v=4?s=100" width="100px;" alt="qq471754603"/><br /><sub><b>qq471754603</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qq471754603" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/san346596324"><img src="https://avatars.githubusercontent.com/u/30828520?v=4?s=100" width="100px;" alt="渭雨"/><br /><sub><b>渭雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=san346596324" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/luoxuanzao"><img src="https://avatars.githubusercontent.com/u/44692579?v=4?s=100" width="100px;" alt="liuxuezhuo"/><br /><sub><b>liuxuezhuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luoxuanzao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ChenXiangxxxxx"><img src="https://avatars.githubusercontent.com/u/90089594?v=4?s=100" width="100px;" alt="ChenXiangxxxxx"/><br /><sub><b>ChenXiangxxxxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChenXiangxxxxx" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Mr-zhou315"><img src="https://avatars.githubusercontent.com/u/10276100?v=4?s=100" width="100px;" alt="Mr.zhou"/><br /><sub><b>Mr.zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Mr-zhou315" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/XimfengYao"><img src="https://avatars.githubusercontent.com/u/17541537?v=4?s=100" width="100px;" alt="姚贤丰"/><br /><sub><b>姚贤丰</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XimfengYao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LINGLUOJUN"><img src="https://avatars.githubusercontent.com/u/16778977?v=4?s=100" width="100px;" alt="lingluojun"/><br /><sub><b>lingluojun</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LINGLUOJUN" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.luelueking.com"><img src="https://avatars.githubusercontent.com/u/93204032?v=4?s=100" width="100px;" alt="1ue"/><br /><sub><b>1ue</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luelueking" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.jimmyqiao.top"><img src="https://avatars.githubusercontent.com/u/67301054?v=4?s=100" width="100px;" alt="qyaaaa"/><br /><sub><b>qyaaaa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qyaaaa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aqyaaaa" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://novohit.top"><img src="https://avatars.githubusercontent.com/u/101090395?v=4?s=100" width="100px;" alt="novohit"/><br /><sub><b>novohit</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=novohit" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/rbsrcy"><img src="https://avatars.githubusercontent.com/u/4798540?v=4?s=100" width="100px;" alt="zhuoshangyi"/><br /><sub><b>zhuoshangyi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=rbsrcy" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ruanliang-hualun"><img src="https://avatars.githubusercontent.com/u/65543716?v=4?s=100" width="100px;" alt="ruanliang"/><br /><sub><b>ruanliang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Eden4701"><img src="https://avatars.githubusercontent.com/u/68422437?v=4?s=100" width="100px;" alt="Eden4701"/><br /><sub><b>Eden4701</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Documentation">📖</a> <a href="#design-Eden4701" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/XiaTian688"><img src="https://avatars.githubusercontent.com/u/111830921?v=4?s=100" width="100px;" alt="XiaTian688"/><br /><sub><b>XiaTian688</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XiaTian688" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/liyin"><img src="https://avatars.githubusercontent.com/u/863169?v=4?s=100" width="100px;" alt="liyinjiang"/><br /><sub><b>liyinjiang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liyin" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jiashu1024"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiashu1024" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ZY945"><img src="https://avatars.githubusercontent.com/u/74083801?v=4?s=100" width="100px;" alt="东风"/><br /><sub><b>东风</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Code">💻</a> <a href="#design-ZY945" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AZY945" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/prolevel1"><img src="https://avatars.githubusercontent.com/u/51995525?v=4?s=100" width="100px;" alt="sonam singh"/><br /><sub><b>sonam singh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=prolevel1" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ZhangZixuan1994"><img src="https://avatars.githubusercontent.com/u/20011653?v=4?s=100" width="100px;" alt="ZhangZixuan1994"/><br /><sub><b>ZhangZixuan1994</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhangZixuan1994" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/hurenjie1"><img src="https://avatars.githubusercontent.com/u/40120355?v=4?s=100" width="100px;" alt="SHIG"/><br /><sub><b>SHIG</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hurenjie1" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://tslj1024.github.io/"><img src="https://avatars.githubusercontent.com/u/155222677?v=4?s=100" width="100px;" alt="泰上老菌"/><br /><sub><b>泰上老菌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tslj1024" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ldysdu"><img src="https://avatars.githubusercontent.com/u/15815338?v=4?s=100" width="100px;" alt="ldysdu"/><br /><sub><b>ldysdu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldysdu" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/GEM0816g"><img src="https://avatars.githubusercontent.com/u/85116017?v=4?s=100" width="100px;" alt="梁同学"/><br /><sub><b>梁同学</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=GEM0816g" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/avvCode"><img src="https://avatars.githubusercontent.com/u/113538532?v=4?s=100" width="100px;" alt="avv"/><br /><sub><b>avv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=avvCode" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yqxxgh"><img src="https://avatars.githubusercontent.com/u/42080876?v=4?s=100" width="100px;" alt="yqxxgh"/><br /><sub><b>yqxxgh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayqxxgh" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieShi46"><img src="https://avatars.githubusercontent.com/u/149798885?v=4?s=100" width="100px;" alt="CharlieShi46"/><br /><sub><b>CharlieShi46</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieShi46" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Nctllnty"><img src="https://avatars.githubusercontent.com/u/33241818?v=4?s=100" width="100px;" alt="Nctllnty"/><br /><sub><b>Nctllnty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Nctllnty" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Wang-Yonghao"><img src="https://avatars.githubusercontent.com/u/48146606?v=4?s=100" width="100px;" alt="Wang-Yonghao"/><br /><sub><b>Wang-Yonghao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Wang-Yonghao" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.yuque.com/dudiao/yy"><img src="https://avatars.githubusercontent.com/u/38355949?v=4?s=100" width="100px;" alt="读钓"/><br /><sub><b>读钓</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dudiao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/starmilkxin"><img src="https://avatars.githubusercontent.com/u/55646681?v=4?s=100" width="100px;" alt="Xin"/><br /><sub><b>Xin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starmilkxin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Astarmilkxin" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/handy-git"><img src="https://avatars.githubusercontent.com/u/32837980?v=4?s=100" width="100px;" alt="handy"/><br /><sub><b>handy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=handy-git" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LiuTianyou"><img src="https://avatars.githubusercontent.com/u/30208283?v=4?s=100" width="100px;" alt="LiuTianyou"/><br /><sub><b>LiuTianyou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALiuTianyou" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Tests">⚠️</a> <a href="#blog-LiuTianyou" title="Blogposts">📝</a> <a href="#design-LiuTianyou" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/WinterKi1ler"><img src="https://avatars.githubusercontent.com/u/160592092?v=4?s=100" width="100px;" alt="WinterKi1ler"/><br /><sub><b>WinterKi1ler</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=WinterKi1ler" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://sharehoo.cn/"><img src="https://avatars.githubusercontent.com/u/45377370?v=4?s=100" width="100px;" alt="miki"/><br /><sub><b>miki</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miki-hmt" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://codeflex.substack.com/"><img src="https://avatars.githubusercontent.com/u/85513042?v=4?s=100" width="100px;" alt="Keshav Carpenter"/><br /><sub><b>Keshav Carpenter</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/makechoicenow"><img src="https://avatars.githubusercontent.com/u/9911918?v=4?s=100" width="100px;" alt="makechoicenow"/><br /><sub><b>makechoicenow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=makechoicenow" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/gjjjj0101"><img src="https://avatars.githubusercontent.com/u/71874373?v=4?s=100" width="100px;" alt="Gao Jian"/><br /><sub><b>Gao Jian</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Documentation">📖</a> <a href="#design-gjjjj0101" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agjjjj0101" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://jangto.tistory.com/"><img src="https://avatars.githubusercontent.com/u/37864182?v=4?s=100" width="100px;" alt="Hyeon Sung"/><br /><sub><b>Hyeon Sung</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://crossoverjie.top/"><img src="https://avatars.githubusercontent.com/u/15684156?v=4?s=100" width="100px;" alt="crossoverJie"/><br /><sub><b>crossoverJie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Documentation">📖</a> <a href="#blog-crossoverJie" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Tests">⚠️</a> <a href="#design-crossoverJie" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/PeixyJ"><img src="https://avatars.githubusercontent.com/u/45998593?v=4?s=100" width="100px;" alt="PeixyJ"/><br /><sub><b>PeixyJ</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PeixyJ" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Hi-Mr-Wind"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="风如歌"/><br /><sub><b>风如歌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Hi-Mr-Wind" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MananPoojara"><img src="https://avatars.githubusercontent.com/u/104253184?v=4?s=100" width="100px;" alt="Manan Pujara"/><br /><sub><b>Manan Pujara</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MananPoojara" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xuziyang"><img src="https://avatars.githubusercontent.com/u/8465969?v=4?s=100" width="100px;" alt="xuziyang"/><br /><sub><b>xuziyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Axuziyang" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lwqzz"><img src="https://avatars.githubusercontent.com/u/62584513?v=4?s=100" width="100px;" alt="lwqzz"/><br /><sub><b>lwqzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwqzz" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/YxYL6125"><img src="https://avatars.githubusercontent.com/u/91076160?v=4?s=100" width="100px;" alt="YxYL"/><br /><sub><b>YxYL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YxYL6125" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/tomorrowshipyltm"><img src="https://avatars.githubusercontent.com/u/61336903?v=4?s=100" width="100px;" alt="tomorrowshipyltm"/><br /><sub><b>tomorrowshipyltm</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomorrowshipyltm" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/15613060203"><img src="https://avatars.githubusercontent.com/u/41351615?v=4?s=100" width="100px;" alt="栗磊"/><br /><sub><b>栗磊</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=15613060203" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Alanxtl"><img src="https://avatars.githubusercontent.com/u/25652981?v=4?s=100" width="100px;" alt="Alan"/><br /><sub><b>Alan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Alanxtl" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.hadoop.wiki/"><img src="https://avatars.githubusercontent.com/u/29418975?v=4?s=100" width="100px;" alt="Jast"/><br /><sub><b>Jast</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Code">💻</a> <a href="#ideas-zhangshenghang" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Documentation">📖</a> <a href="#blog-zhangshenghang" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azhangshenghang" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Tests">⚠️</a> <a href="#design-zhangshenghang" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zuobiao-zhou"><img src="https://avatars.githubusercontent.com/u/61108539?v=4?s=100" width="100px;" alt="Zhang Yuxuan"/><br /><sub><b>Zhang Yuxuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azuobiao-zhou" title="Bug reports">🐛</a> <a href="#blog-zuobiao-zhou" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Pzz-2021"><img src="https://avatars.githubusercontent.com/u/118056735?v=4?s=100" width="100px;" alt="P.P."/><br /><sub><b>P.P.</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Pzz-2021" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LLP2333"><img src="https://avatars.githubusercontent.com/u/61670545?v=4?s=100" width="100px;" alt="llp2333"/><br /><sub><b>llp2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LLP2333" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/HeartLinked"><img src="https://avatars.githubusercontent.com/u/78212101?v=4?s=100" width="100px;" alt="feiyang li"/><br /><sub><b>feiyang li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HeartLinked" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Aias00"><img src="https://avatars.githubusercontent.com/u/25810623?v=4?s=100" width="100px;" alt="aias00"/><br /><sub><b>aias00</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AAias00" title="Bug reports">🐛</a> <a href="#ideas-Aias00" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/westboy"><img src="https://avatars.githubusercontent.com/u/6385565?v=4?s=100" width="100px;" alt="Jin"/><br /><sub><b>Jin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=westboy" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://blog.csdn.net/qq_52397471"><img src="https://avatars.githubusercontent.com/u/77964041?v=4?s=100" width="100px;" alt="YuLuo"/><br /><sub><b>YuLuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayuluo-yx" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Tests">⚠️</a> <a href="#blog-yuluo-yx" title="Blogposts">📝</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Yanshuming1"><img src="https://avatars.githubusercontent.com/u/118667222?v=4?s=100" width="100px;" alt="linDong"/><br /><sub><b>linDong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AYanshuming1" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lwjxy"><img src="https://avatars.githubusercontent.com/u/52726400?v=4?s=100" width="100px;" alt="lwjxy"/><br /><sub><b>lwjxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwjxy" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://thespica.github.io/"><img src="https://avatars.githubusercontent.com/u/119573640?v=4?s=100" width="100px;" alt="John"/><br /><sub><b>John</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/boatrainlsz"><img src="https://avatars.githubusercontent.com/u/18243785?v=4?s=100" width="100px;" alt="boatrainlsz"/><br /><sub><b>boatrainlsz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boatrainlsz" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.yitianyigexiangfa.com/"><img src="https://avatars.githubusercontent.com/u/3973419?v=4?s=100" width="100px;" alt="Bill Lau"/><br /><sub><b>Bill Lau</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaProgrammerLB" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lw-yang"><img src="https://avatars.githubusercontent.com/u/23456873?v=4?s=100" width="100px;" alt="lwyang"/><br /><sub><b>lwyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lw-yang" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xfl12345"><img src="https://avatars.githubusercontent.com/u/17960863?v=4?s=100" width="100px;" alt="xfl12345"/><br /><sub><b>xfl12345</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xfl12345" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yykaue"><img src="https://avatars.githubusercontent.com/u/22905143?v=4?s=100" width="100px;" alt="Limbo"/><br /><sub><b>Limbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yykaue" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/irenhongyan"><img src="https://avatars.githubusercontent.com/u/53438321?v=4?s=100" width="100px;" alt="哈哈哈哈哈哈哈哈哈"/><br /><sub><b>哈哈哈哈哈哈哈哈哈</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=irenhongyan" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ileonli"><img src="https://avatars.githubusercontent.com/u/45332412?v=4?s=100" width="100px;" alt="Leon Li"/><br /><sub><b>Leon Li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ileonli" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://fnil.net/"><img src="https://avatars.githubusercontent.com/u/14142?v=4?s=100" width="100px;" alt="dennis zhuang"/><br /><sub><b>dennis zhuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=killme2008" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/kerwin612"><img src="https://avatars.githubusercontent.com/u/3371163?v=4?s=100" width="100px;" alt="Kerwin Bryant"/><br /><sub><b>Kerwin Bryant</b></sub></a><br /><a href="#design-kerwin612" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akerwin612" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ShineDevelopment"><img src="https://avatars.githubusercontent.com/u/59306780?v=4?s=100" width="100px;" alt="daixianglong"/><br /><sub><b>daixianglong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShineDevelopment" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/mchgood"><img src="https://avatars.githubusercontent.com/u/38482005?v=4?s=100" width="100px;" alt="mchgood"/><br /><sub><b>mchgood</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mchgood" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/pwallk"><img src="https://avatars.githubusercontent.com/u/69385076?v=4?s=100" width="100px;" alt="kangli"/><br /><sub><b>kangli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apwallk" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/cdphantom"><img src="https://avatars.githubusercontent.com/u/12674795?v=4?s=100" width="100px;" alt="cdphantom"/><br /><sub><b>cdphantom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cdphantom" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/asd108908382"><img src="https://avatars.githubusercontent.com/u/77717999?v=4?s=100" width="100px;" alt="jiawei.guo"/><br /><sub><b>jiawei.guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=asd108908382" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/QBH-insist"><img src="https://avatars.githubusercontent.com/u/39401478?v=4?s=100" width="100px;" alt="QBH-insist"/><br /><sub><b>QBH-insist</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QBH-insist" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jiangsh-ui"><img src="https://avatars.githubusercontent.com/u/86990361?v=4?s=100" width="100px;" alt="jiangsh"/><br /><sub><b>jiangsh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiangsh-ui" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/keaifafafa"><img src="https://avatars.githubusercontent.com/u/83876361?v=4?s=100" width="100px;" alt="Keaifa"/><br /><sub><b>Keaifa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=keaifafafa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akeaifafafa" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/loong95"><img src="https://avatars.githubusercontent.com/u/16333958?v=4?s=100" width="100px;" alt="Loong"/><br /><sub><b>Loong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=loong95" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ceekay47"><img src="https://avatars.githubusercontent.com/u/104664857?v=4?s=100" width="100px;" alt="Chandrakant Vankayalapati"/><br /><sub><b>Chandrakant Vankayalapati</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ceekay47" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MRgenial"><img src="https://avatars.githubusercontent.com/u/49973336?v=4?s=100" width="100px;" alt="b_mountain"/><br /><sub><b>b_mountain</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MRgenial" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/TemirlanBasitov"><img src="https://avatars.githubusercontent.com/u/57500808?v=4?s=100" width="100px;" alt="TemirlanBasitov"/><br /><sub><b>TemirlanBasitov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TemirlanBasitov" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wyfvsfy"><img src="https://avatars.githubusercontent.com/u/11973517?v=4?s=100" width="100px;" alt="wyfvsfy"/><br /><sub><b>wyfvsfy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyfvsfy" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/sherry-peng2333"><img src="https://avatars.githubusercontent.com/u/70619577?v=4?s=100" width="100px;" alt="sherry-peng2333"/><br /><sub><b>sherry-peng2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sherry-peng2333" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lixiaobaivv"><img src="https://avatars.githubusercontent.com/u/39290771?v=4?s=100" width="100px;" alt="Yzzz"/><br /><sub><b>Yzzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lixiaobaivv" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.bckf.cn/"><img src="https://avatars.githubusercontent.com/u/13309008?v=4?s=100" width="100px;" alt="puruidong"/><br /><sub><b>puruidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pruidong" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/shinestare"><img src="https://avatars.githubusercontent.com/u/13570619?v=4?s=100" width="100px;" alt="shinestare"/><br /><sub><b>shinestare</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shinestare" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/po-168"><img src="https://avatars.githubusercontent.com/u/185745593?v=4?s=100" width="100px;" alt="po-168"/><br /><sub><b>po-168</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=po-168" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/All-The-Best-for"><img src="https://avatars.githubusercontent.com/u/76414672?v=4?s=100" width="100px;" alt="wbs99"/><br /><sub><b>wbs99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=All-The-Best-for" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/starryCoder"><img src="https://avatars.githubusercontent.com/u/46510059?v=4?s=100" width="100px;" alt="starryCoder"/><br /><sub><b>starryCoder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starryCoder" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/hasimmollah"><img src="https://avatars.githubusercontent.com/u/32538599?v=4?s=100" width="100px;" alt="hasimmollah"/><br /><sub><b>hasimmollah</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hasimmollah" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ayu-v0"><img src="https://avatars.githubusercontent.com/u/127600988?v=4?s=100" width="100px;" alt="Ayu"/><br /><sub><b>Ayu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ayu-v0" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Rancho-7"><img src="https://avatars.githubusercontent.com/u/59016860?v=4?s=100" width="100px;" alt="Nick Guo"/><br /><sub><b>Nick Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ARancho-7" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/doveLin0818"><img src="https://avatars.githubusercontent.com/u/190927907?v=4?s=100" width="100px;" alt="doveLin"/><br /><sub><b>doveLin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=doveLin0818" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://zzrl.cc/"><img src="https://avatars.githubusercontent.com/u/91836599?v=4?s=100" width="100px;" alt="yunfan24"/><br /><sub><b>yunfan24</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayunfan24" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Tests">⚠️</a> <a href="#blog-yunfan24" title="Blogposts">📝</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lctking"><img src="https://avatars.githubusercontent.com/u/168249998?v=4?s=100" width="100px;" alt="nullwli"/><br /><sub><b>nullwli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lctking" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://simonsigre.com/"><img src="https://avatars.githubusercontent.com/u/14932913?v=4?s=100" width="100px;" alt="Simon Sigré"/><br /><sub><b>Simon Sigré</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.ponfee.cn/"><img src="https://avatars.githubusercontent.com/u/46117331?v=4?s=100" width="100px;" alt="ponfee"/><br /><sub><b>ponfee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ponfee" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Vedant7789"><img src="https://avatars.githubusercontent.com/u/147625492?v=4?s=100" width="100px;" alt="Vedant7789"/><br /><sub><b>Vedant7789</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Vedant7789" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Craaaaazy77"><img src="https://avatars.githubusercontent.com/u/23025522?v=4?s=100" width="100px;" alt="Craaaaazy77"/><br /><sub><b>Craaaaazy77</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Craaaaazy77" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Suvrat1629"><img src="https://avatars.githubusercontent.com/u/140749446?v=4?s=100" width="100px;" alt="Suvrat1629"/><br /><sub><b>Suvrat1629</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Suvrat1629" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://ghyghoo8.github.io/"><img src="https://avatars.githubusercontent.com/u/363129?v=4?s=100" width="100px;" alt="ghy"/><br /><sub><b>ghy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ghyghoo8" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/helei1030"><img src="https://avatars.githubusercontent.com/u/11839080?v=4?s=100" width="100px;" alt="helei1030"/><br /><sub><b>helei1030</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=helei1030" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://medium.com/@pjfanning"><img src="https://avatars.githubusercontent.com/u/11783444?v=4?s=100" width="100px;" alt="PJ Fanning"/><br /><sub><b>PJ Fanning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apjfanning" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MonsterChenzhuo"><img src="https://avatars.githubusercontent.com/u/60029759?v=4?s=100" width="100px;" alt="monster"/><br /><sub><b>monster</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MonsterChenzhuo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MasamiYui"><img src="https://avatars.githubusercontent.com/u/22274133?v=4?s=100" width="100px;" alt="Sherlock Yin"/><br /><sub><b>Sherlock Yin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AMasamiYui" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wanhao23"><img src="https://avatars.githubusercontent.com/u/29560961?v=4?s=100" width="100px;" alt="wanhao"/><br /><sub><b>wanhao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jonasHanhan"><img src="https://avatars.githubusercontent.com/u/130035609?v=4?s=100" width="100px;" alt="jonasHanhan"/><br /><sub><b>jonasHanhan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jonasHanhan" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/NikhilMurugesan"><img src="https://avatars.githubusercontent.com/u/49281792?v=4?s=100" width="100px;" alt="NikhilMurugesan"/><br /><sub><b>NikhilMurugesan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NikhilMurugesan" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/myangle1120"><img src="https://avatars.githubusercontent.com/u/19237013?v=4?s=100" width="100px;" alt="myangle1120"/><br /><sub><b>myangle1120</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=myangle1120" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Anotbugggg" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Documentation">📖</a> <a href="#ideas-JuJinPark" title="Ideas, Planning, & Feedback">🤔</a> <a href="#blog-JuJinPark" title="Blogposts">📝</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALL-LIN" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abigcyy" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td> + </tr> + </tbody> +</table> + +<!-- markdownlint-restore --> +<!-- prettier-ignore-end --> + +<!-- ALL-CONTRIBUTORS-LIST:END --> + +## 💬 社区交流 + +##### Channel + +[订阅邮件列表](https://lists.apache.org/list.html?dev@hertzbeat.apache.org) : 发送邮件至 `dev-subscribe@hertzbeat.apache.org` 来订阅邮件列表. + +[Chat On Discord](https://discord.gg/Fb6M73htGr) + +微信交流群 : 加 `ahertzbeat` 好友邀请进群. + +微信公众号 : 搜索 ID `usthecom`. + +[Github Discussion](https://github.com/apache/hertzbeat/discussions) + +[Follow Us Twitter](https://x.com/hertzbeat1024) + +[Subscribe YouTube](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow) + + +##### Open-Source Project Build From Open-Source + +HertzBeat is built on so many great open source projects, thanks to them! + +- `Java Spring SpringBoot Jpa Maven Assembly Netty Lombok Sureness Protobuf HttpClient Guava SnakeYaml JsonPath ...` +- `TypeScript Angular NG-ZORRO NG-ALAIN NodeJs Npm Html Less Echarts Rxjs ZoneJs MonacoEditor SlickCarousel Docusaurus ...` + + +## Landscape + +<p align="left"> +<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300">  <img src="./home/static/img/home/cncf-right-logo.svg" width="345" /> +<br /><br /> +HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat"> +CNCF Observability And Analysis - Observability Landscape.</a> +</p> + +## 🛡️ License +[`Apache License, Version 2.0`](https://www.apache.org/licenses/LICENSE-2.0.html) diff --git a/README_JP.md b/README_JP.md new file mode 100644 index 0000000..565cf48 --- /dev/null +++ b/README_JP.md @@ -0,0 +1,600 @@ +<p align="center"> + <a href="https://hertzbeat.apache.org"> + <img alt="hertzbeat" src="/home/static/img/hertzbeat-brand.svg" width="260"> + </a> +</p> + +<p align="center"> +<b>Readme</b>: +<a href="README.md">English</a> | <a href="README_CN.md">中文</a> | <b><a href="README_JP.md">日本語</a></b> +</p> + +[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr) +[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://x.com/hertzbeat1024) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8139/badge)](https://www.bestpractices.dev/projects/8139) +[![codecov](https://codecov.io/gh/apache/HertzBeat/branch/master/graph/badge.svg)](https://app.codecov.io/gh/apache/hertzbeat) +[![Docker Pulls](https://img.shields.io/docker/pulls/apache/hertzbeat?style=%20for-the-badge&logo=docker&label=DockerHub%20Download)](https://hub.docker.com/r/apache/hertzbeat) +[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/hertzbeat)](https://artifacthub.io/packages/search?repo=hertzbeat) +[![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCri75zfWX0GHqJFPENEbLow?logo=youtube&label=YouTube%20Channel)](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow) +[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/apache/hertzbeat) + + +**公式サイト: [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 駆動の次世代オープンソースリアルタイム観測システムです。メトリクスとログの統一収集、アラートの一元配信、インテリジェント管理分析。エージェント不要、高性能クラスタ、強力なカスタム監視とステータスページ構築機能を提供します。 + +### 特性 + +- **収集+分析+アラート+通知**を一つのプラットフォームに統合し、HertzBeat AI 駆動の新しいインタラクションと機能、内蔵 MCP Server 機能を提供。 +- 統一メトリクスプラットフォーム、エージェントレス、Prometheus互換、アプリケーションサービス、プログラム、データベース、キャッシュ、オペレーティングシステム、ビッグデータ、ミドルウェア、Webサーバー、クラウドネイティブ、ネットワーク、カスタム監視などをサポート。 +- 統一ログプラットフォーム、OTLP プロトコルを通じて複数のログソースをシームレスに統合してレポート。 +- 統一アラートプラットフォーム、内部アラートと様々な外部アラートソースを統合接続、統一アラート処理分析、柔軟なリアルタイムと周期的閾値ルール、グループ収束、サイレンス、抑制など。 +- 統一メッセージ配信、アラートプラットフォームで処理後、`メール` `Discord` `Slack` `Telegram` `DingTalk` `WeChat` `FeiShu` `SMS` `Webhook` `ServerChan` などの方法で配信通知。 +- `Http、Jmx、Ssh、Snmp、Jdbc、Prometheus`などのプロトコルを設定可能にし、テンプレート`YML`ファイルをオンラインで設定するだけで、あらゆるメトリクスを収集できるようにします。HertzBeatでオンライン設定するだけで、`K8s`や`Docker`のような新しい監視タイプに素早く対応できることを想像してみてください。 +- 高性能で、コレクタークラスタの水平拡張、マルチ分離ネットワーク監視、クラウドエッジ協調をサポート。 +- 強力なステータスページ構築機能を提供し、サービスのリアルタイムステータスをユーザーに簡単に伝達。 + +> `HertzBeat`の統一プラットフォーム、AI インテリジェンス、強力なカスタマイズ、多種類サポート、高性能、容易な拡張性は、ユーザーが迅速かつ便利に観測要件を実現することを支援することを目的としています。 + +---- + +---- + +## 🥐 モジュール + +![hertzBeat](home/static/img/docs/hertzbeat-arch.png) + +## ⛄ 利用可能 + +> `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 をローカルに展開する場合は、以下のデプロイメントドキュメントを参照してください。 + +### 🍞 HertzBeatのインストール +> HertzBeatは、ソースコードのインストールとブート、Dockerコンテナの実行とインストールパッケージによるインストールとデプロイをサポートし、CPUアーキテクチャはx86/arm64をサポートします。 + +##### 方式1:Docker + +1. `docker` で以下の指令を実行します: + + ```shell + docker run -d -p 1157:1157 -p 1158:1158 --name hertzbeat apache/hertzbeat + ``` + +2. スタート:`http://localhost:1157`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。 + +3. コレクタークラスタのデプロイメント(オプション) + + ```shell + docker run -d -e IDENTITY=custom-collector-name -e MANAGER_HOST=127.0.0.1 -e MANAGER_PORT=1158 --name hertzbeat-collector apache/hertzbeat-collector + ``` + + - `-e IDENTITY=custom-collector-name` :コレクターのユニーク ID。 + - `-e MODE=public` :実行モード(パブリッククラスタまたはプライベートクラウドエッジ)。 + - `-e MANAGER_HOST=127.0.0.1` :メインhertzbeatサーバーのIP。 + - `-e MANAGER_PORT=1158` :メインhertzbeatサーバポート。 + + +詳細ステップ [Dockerで HertzBeat をインストール](https://hertzbeat.apache.org/docs/start/docker-deploy) + +##### 方式2:インストールパッケージ + +1. リリースパッケージ `apache-hertzbeat-xx-bin.tar.gz` をダウンロードします [Download](https://hertzbeat.apache.org/docs/download) +2. HertzBeat の設定ファイル `hertzbeat/config/application.yml` を編集します(任意) +3. コマンド `$ ./bin/startup.sh` または `bin/startup.bat` を実行します +4. ブラウザで `http://localhost:1157` にアクセスします。デフォルトのアカウントとパスワードは `admin/hertzbeat` です +5. コレクタークラスタのデプロイメント(オプション) + - 別ホストにコレクターのインストールパッケージ `apache-hertzbeat-collector-xx-bin.tar.gz`(JVM コレクター)または対象プラットフォーム向けの Native コレクターパッケージ(例: `apache-hertzbeat-collector-native-xx-linux-amd64-bin.tar.gz`、`apache-hertzbeat-collector-native-xx-windows-amd64-bin.zip`)をダウンロードします [Download](https://hertzbeat.apache.org/docs/download) + - コレクターの設定ファイル `hertzbeat-collector/config/application.yml` を編集します + ```yaml + collector: + dispatch: + entrance: + netty: + enabled: true + identity: ${IDENTITY:} + mode: ${MODE:public} + manager-host: ${MANAGER_HOST:127.0.0.1} + manager-port: ${MANAGER_PORT:1158} + ``` + - `identity: ${IDENTITY:}`:コレクターのユニークID。 + - `mode: ${MODE:public}`:実行モード(パブリッククラスタまたはプライベートクラウドエッジ)。 + - `manager-host: ${MANAGER_HOST:127.0.0.1}`:メインhertzbeatサーバーのIP。 + - `manager-port: ${MANAGER_PORT:1158}`:メインhertzbeatサーバポート。 + - `ext-lib` に JDBC ドライバーを置かない場合、MySQL、MariaDB、OceanBase は組み込みのクエリエンジンを使って Native コレクターパッケージでも監視できます。TiDB も SQL クエリのメトリクスセットについては同じルールです。 + - `ext-lib` に `mysql-connector-j` を置いた場合は、再起動後に組み込みサーバーコレクターまたは JVM コレクターが MySQL、MariaDB、OceanBase で自動的に JDBC を優先します。TiDB も SQL クエリのメトリクスセットについては同じルールで、HTTP メトリクスは影響を受けません。Oracle と DB2 は引き続き外部 JDBC ドライバーに依存するため、JVM コレクターパッケージを使用してください。 + - JVM コレクターのインストールパッケージは `$ ./bin/startup.sh` または `bin/startup.bat`、Linux/macOS の Native コレクターパッケージは `$ ./bin/startup.sh`、Windows の Native コレクターパッケージは `bin\\startup.bat` で起動します。 + - メインの HertzBeat サービス `http://localhost:1157` にアクセスすると、登録された新しいコレクターを確認できます。 + +詳細ステップ [インストールパッケージで HertzBeat をインストール](https://hertzbeat.apache.org/docs/start/package-deploy) + +##### 方式3:ローカルの実行 + +1. ローカルの実行には、バックエンドのプロジェクト`hertzbeat-startup`とフロントエンドのプロジェクト`web-app`を起動する必要があります。 +2. バックエンド:`maven3+`、`Java25`、`lombok` の環境が必要です。`YML` 設定を修正し、Java 仮想マシンパラメータに `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加して `hertzbeat-startup` を起動します。 +3. フロントエンド:`nodejs` と `pnpm` の環境が必要です。ローカルのバックエンドが立ち上がったら、`web-app` ディレクトリで `pnpm install` を実行し、続けて `pnpm start` を実行します。 +4. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。 + +詳細ステップ [貢献ガイド](CONTRIBUTING.md) + +##### 方式4:Docker-Compose + +[Docker-Compose 部署脚本](script/docker-compose)でpostgresql/mysqlデータベース、victoria-metrics、iotdb、またはtdengine時系列データベースとHertzBeat一括デプロイ。 + +詳細ステップ [Docker-Compose で HertzBeat をインストール](script/docker-compose/README.md) + +##### 方式5:Kubernetes Helm Charts + +Helm ChartでHertzBeatクラスタコンポーネントをKubernetesクラスタに一括デプロイ。 + +詳細ステップ [Artifact Hub](https://artifacthub.io/packages/helm/hertzbeat/hertzbeat) + +**HAVE FUN** + +## ✨ Contributors + +Thanks these wonderful people, welcome to join us: +[貢献ガイド](CONTRIBUTING.md) + +<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> +<!-- prettier-ignore-start --> +<!-- markdownlint-disable --> +<table> + <tbody> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/tomsun28"><img src="https://avatars.githubusercontent.com/u/24788200?v=4?s=100" width="100px;" alt="tomsun28"/><br /><sub><b>tomsun28</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Documentation">📖</a> <a href="#design-tomsun28" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wang1027-wqh"><img src="https://avatars.githubusercontent.com/u/71161318?v=4?s=100" width="100px;" alt="会编程的王学长"/><br /><sub><b>会编程的王学长</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Documentation">📖</a> <a href="#design-wang1027-wqh" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.maxkey.top/"><img src="https://avatars.githubusercontent.com/u/1563377?v=4?s=100" width="100px;" alt="MaxKey"/><br /><sub><b>MaxKey</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shimingxy" title="Code">💻</a> <a href="#design-shimingxy" title="Design">🎨</a> <a href="#ideas-shimingxy" title="Ideas, Planning, & Feedback">🤔</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://blog.gcdd.top/"><img src="https://avatars.githubusercontent.com/u/26523525?v=4?s=100" width="100px;" alt="观沧海"/><br /><sub><b>观沧海</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gcdd1993" title="Code">💻</a> <a href="#design-gcdd1993" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agcdd1993" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/a25017012"><img src="https://avatars.githubusercontent.com/u/32265356?v=4?s=100" width="100px;" alt="yuye"/><br /><sub><b>yuye</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jx10086"><img src="https://avatars.githubusercontent.com/u/5323228?v=4?s=100" width="100px;" alt="jx10086"/><br /><sub><b>jx10086</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx10086" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ajx10086" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/winnerTimer"><img src="https://avatars.githubusercontent.com/u/76024658?v=4?s=100" width="100px;" alt="winnerTimer"/><br /><sub><b>winnerTimer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=winnerTimer" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AwinnerTimer" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/goo-kits"><img src="https://avatars.githubusercontent.com/u/13163673?v=4?s=100" width="100px;" alt="goo-kits"/><br /><sub><b>goo-kits</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=goo-kits" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agoo-kits" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/brave4Time"><img src="https://avatars.githubusercontent.com/u/105094014?v=4?s=100" width="100px;" alt="brave4Time"/><br /><sub><b>brave4Time</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brave4Time" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abrave4Time" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/walkerlee-lab"><img src="https://avatars.githubusercontent.com/u/8426753?v=4?s=100" width="100px;" alt="WalkerLee"/><br /><sub><b>WalkerLee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=walkerlee-lab" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Awalkerlee-lab" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/fullofjoy"><img src="https://avatars.githubusercontent.com/u/30247571?v=4?s=100" width="100px;" alt="jianghang"/><br /><sub><b>jianghang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fullofjoy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Afullofjoy" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ChineseTony"><img src="https://avatars.githubusercontent.com/u/24618786?v=4?s=100" width="100px;" alt="ChineseTony"/><br /><sub><b>ChineseTony</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChineseTony" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AChineseTony" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt199905"><img src="https://avatars.githubusercontent.com/u/85098809?v=4?s=100" width="100px;" alt="wyt199905"/><br /><sub><b>wyt199905</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt199905" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/weifuqing"><img src="https://avatars.githubusercontent.com/u/13931013?v=4?s=100" width="100px;" alt="卫傅庆"/><br /><sub><b>卫傅庆</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weifuqing" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aweifuqing" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zklmcookle"><img src="https://avatars.githubusercontent.com/u/107192352?v=4?s=100" width="100px;" alt="zklmcookle"/><br /><sub><b>zklmcookle</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zklmcookle" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/DevilX5"><img src="https://avatars.githubusercontent.com/u/13269921?v=4?s=100" width="100px;" alt="DevilX5"/><br /><sub><b>DevilX5</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/djzeng"><img src="https://avatars.githubusercontent.com/u/14074864?v=4?s=100" width="100px;" alt="tea"/><br /><sub><b>tea</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=djzeng" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yangshihui"><img src="https://avatars.githubusercontent.com/u/28550208?v=4?s=100" width="100px;" alt="yangshihui"/><br /><sub><b>yangshihui</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yangshihui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayangshihui" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/DreamGirl524"><img src="https://avatars.githubusercontent.com/u/81132838?v=4?s=100" width="100px;" alt="DreamGirl524"/><br /><sub><b>DreamGirl524</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/gzwlly"><img src="https://avatars.githubusercontent.com/u/83171907?v=4?s=100" width="100px;" alt="gzwlly"/><br /><sub><b>gzwlly</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gzwlly" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/cuipiheqiuqiu"><img src="https://avatars.githubusercontent.com/u/76642201?v=4?s=100" width="100px;" alt="cuipiheqiuqiu"/><br /><sub><b>cuipiheqiuqiu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Tests">⚠️</a> <a href="#design-cuipiheqiuqiu" title="Design">🎨</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/oyiyou"><img src="https://avatars.githubusercontent.com/u/39228891?v=4?s=100" width="100px;" alt="lambert"/><br /><sub><b>lambert</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=oyiyou" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://mroldx.xyz/"><img src="https://avatars.githubusercontent.com/u/34847828?v=4?s=100" width="100px;" alt="mroldx"/><br /><sub><b>mroldx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mroldx" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/woshiniusange"><img src="https://avatars.githubusercontent.com/u/91513022?v=4?s=100" width="100px;" alt="woshiniusange"/><br /><sub><b>woshiniusange</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=woshiniusange" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://vampireachao.github.io/"><img src="https://avatars.githubusercontent.com/u/52746628?v=4?s=100" width="100px;" alt="VampireAchao"/><br /><sub><b>VampireAchao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=VampireAchao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Ceilzcx"><img src="https://avatars.githubusercontent.com/u/48920254?v=4?s=100" width="100px;" alt="zcx"/><br /><sub><b>zcx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACeilzcx" title="Bug reports">🐛</a> <a href="#design-Ceilzcx" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Tests">⚠️</a> <a href="#blog-Ceilzcx" title="Blogposts">📝</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieXCL"><img src="https://avatars.githubusercontent.com/u/91540487?v=4?s=100" width="100px;" alt="CharlieXCL"/><br /><sub><b>CharlieXCL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieXCL" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Privauto"><img src="https://avatars.githubusercontent.com/u/36581456?v=4?s=100" width="100px;" alt="Privauto"/><br /><sub><b>Privauto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/emrys-he"><img src="https://avatars.githubusercontent.com/u/5848915?v=4?s=100" width="100px;" alt="emrys"/><br /><sub><b>emrys</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=emrys-he" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/SxLiuYu"><img src="https://avatars.githubusercontent.com/u/95198625?v=4?s=100" width="100px;" alt="SxLiuYu"/><br /><sub><b>SxLiuYu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ASxLiuYu" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://allcontributors.org"><img src="https://avatars.githubusercontent.com/u/46410174?v=4?s=100" width="100px;" alt="All Contributors"/><br /><sub><b>All Contributors</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=all-contributors" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/gxc-myh"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/click33"><img src="https://avatars.githubusercontent.com/u/36243476?v=4?s=100" width="100px;" alt="click33"/><br /><sub><b>click33</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=click33" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://jpom.io/"><img src="https://avatars.githubusercontent.com/u/16408873?v=4?s=100" width="100px;" alt="蒋小小"/><br /><sub><b>蒋小小</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bwcx-jzy" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.zhihu.com/people/kevinbauer"><img src="https://avatars.githubusercontent.com/u/28581579?v=4?s=100" width="100px;" alt="Kevin Huang"/><br /><sub><b>Kevin Huang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=kevinhuangwl" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/TJxiaobao"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ATJxiaobao" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Tests">⚠️</a> <a href="#design-TJxiaobao" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Jack-123-power"><img src="https://avatars.githubusercontent.com/u/84333501?v=4?s=100" width="100px;" alt="Captain Jack"/><br /><sub><b>Captain Jack</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jack-123-power" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/haibo-duan"><img src="https://avatars.githubusercontent.com/u/7974845?v=4?s=100" width="100px;" alt="haibo.duan"/><br /><sub><b>haibo.duan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/assassinfym"><img src="https://avatars.githubusercontent.com/u/15188754?v=4?s=100" width="100px;" alt="assassin"/><br /><sub><b>assassin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Aassassinfym" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=assassinfym" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/csyshu"><img src="https://avatars.githubusercontent.com/u/46591658?v=4?s=100" width="100px;" alt="Reverse wind"/><br /><sub><b>Reverse wind</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/luxx-lq"><img src="https://avatars.githubusercontent.com/u/58515565?v=4?s=100" width="100px;" alt="luxx"/><br /><sub><b>luxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luxx-lq" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eltociear" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zenan08"><img src="https://avatars.githubusercontent.com/u/80514991?v=4?s=100" width="100px;" alt="leizenan"/><br /><sub><b>leizenan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zenan08" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/BKing2020"><img src="https://avatars.githubusercontent.com/u/28869121?v=4?s=100" width="100px;" alt="BKing"/><br /><sub><b>BKing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BKing2020" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xingshuaiLi"><img src="https://avatars.githubusercontent.com/u/119487588?v=4?s=100" width="100px;" alt="xingshuaiLi"/><br /><sub><b>xingshuaiLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xingshuaiLi" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wangke6666"><img src="https://avatars.githubusercontent.com/u/113656595?v=4?s=100" width="100px;" alt="wangke6666"/><br /><sub><b>wangke6666</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wangke6666" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LWBobo"><img src="https://avatars.githubusercontent.com/u/50368698?v=4?s=100" width="100px;" alt="刺猬"/><br /><sub><b>刺猬</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ALWBobo" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LWBobo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.zanglikun.com"><img src="https://avatars.githubusercontent.com/u/61591648?v=4?s=100" width="100px;" alt="Haste"/><br /><sub><b>Haste</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zanglikun" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/SuitSmile"><img src="https://avatars.githubusercontent.com/u/38679717?v=4?s=100" width="100px;" alt="zhongshi.yi"/><br /><sub><b>zhongshi.yi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SuitSmile" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://www.smallq.cn"><img src="https://avatars.githubusercontent.com/u/39754275?v=4?s=100" width="100px;" alt="Qi Zhang"/><br /><sub><b>Qi Zhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zzzhangqi" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MrAndyMing"><img src="https://avatars.githubusercontent.com/u/49541483?v=4?s=100" width="100px;" alt="MrAndyMing"/><br /><sub><b>MrAndyMing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrAndyMing" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://idongliming.github.io/"><img src="https://avatars.githubusercontent.com/u/31564353?v=4?s=100" width="100px;" alt="idongliming"/><br /><sub><b>idongliming</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=idongliming" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://earthjasonlin.github.io"><img src="https://avatars.githubusercontent.com/u/83632110?v=4?s=100" width="100px;" alt="Zichao Lin"/><br /><sub><b>Zichao Lin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://blog.liudonghua.com"><img src="https://avatars.githubusercontent.com/u/2276718?v=4?s=100" width="100px;" alt="liudonghua"/><br /><sub><b>liudonghua</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liudonghua123" title="Code">💻</a> <a href="#ideas-liudonghua123" title="Ideas, Planning, & Feedback">🤔</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeyts"><img src="https://avatars.githubusercontent.com/u/4250869?v=4?s=100" width="100px;" alt="Jerry"/><br /><sub><b>Jerry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Tests">⚠️</a> <a href="#ideas-orangeyts" title="Ideas, Planning, & Feedback">🤔</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://dynamictp.cn"><img src="https://avatars.githubusercontent.com/u/13051908?v=4?s=100" width="100px;" alt="yanhom"/><br /><sub><b>yanhom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yanhom1314" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://www.jianshu.com/u/a8f822c04f67"><img src="https://avatars.githubusercontent.com/u/18587688?v=4?s=100" width="100px;" alt="fsl"/><br /><sub><b>fsl</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengshunli" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xttttv"><img src="https://avatars.githubusercontent.com/u/116323904?v=4?s=100" width="100px;" alt="xttttv"/><br /><sub><b>xttttv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xttttv" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/NavinKumarBarnwal"><img src="https://avatars.githubusercontent.com/u/44504274?v=4?s=100" width="100px;" alt="NavinKumarBarnwal"/><br /><sub><b>NavinKumarBarnwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NavinKumarBarnwal" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/z641205699"><img src="https://avatars.githubusercontent.com/u/45276423?v=4?s=100" width="100px;" alt="Zakkary"/><br /><sub><b>Zakkary</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=z641205699" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/898349230"><img src="https://avatars.githubusercontent.com/u/21972532?v=4?s=100" width="100px;" alt="sunxinbo"/><br /><sub><b>sunxinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ldzbook"><img src="https://avatars.githubusercontent.com/u/13903790?v=4?s=100" width="100px;" alt="ldzbook"/><br /><sub><b>ldzbook</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldzbook" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aldzbook" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/SurryChen"><img src="https://avatars.githubusercontent.com/u/91116490?v=4?s=100" width="100px;" alt="余与雨"/><br /><sub><b>余与雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Tests">⚠️</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MysticalDream"><img src="https://avatars.githubusercontent.com/u/78899028?v=4?s=100" width="100px;" alt="MysticalDream"/><br /><sub><b>MysticalDream</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zhouyoulin12"><img src="https://avatars.githubusercontent.com/u/17086633?v=4?s=100" width="100px;" alt="zhouyoulin12"/><br /><sub><b>zhouyoulin12</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jerjjj"><img src="https://avatars.githubusercontent.com/u/93431283?v=4?s=100" width="100px;" alt="jerjjj"/><br /><sub><b>jerjjj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jerjjj" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://wjl110.xyz/"><img src="https://avatars.githubusercontent.com/u/53851034?v=4?s=100" width="100px;" alt="wjl110"/><br /><sub><b>wjl110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wjl110" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ngyhd"><img src="https://avatars.githubusercontent.com/u/29095207?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ngyhd" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Daydreamer-ia"><img src="https://avatars.githubusercontent.com/u/83362909?v=4?s=100" width="100px;" alt="chenyiqin"/><br /><sub><b>chenyiqin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/hudongdong129"><img src="https://avatars.githubusercontent.com/u/34374227?v=4?s=100" width="100px;" alt="hudongdong129"/><br /><sub><b>hudongdong129</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Documentation">📖</a> <a href="#design-hudongdong129" title="Design">🎨</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/TherChenYang"><img src="https://avatars.githubusercontent.com/u/124348939?v=4?s=100" width="100px;" alt="TherChenYang"/><br /><sub><b>TherChenYang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/HattoriHenzo"><img src="https://avatars.githubusercontent.com/u/5141285?v=4?s=100" width="100px;" alt="HattoriHenzo"/><br /><sub><b>HattoriHenzo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ycilry"><img src="https://avatars.githubusercontent.com/u/63967101?v=4?s=100" width="100px;" alt="ycilry"/><br /><sub><b>ycilry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ycilry" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/aoshiguchen"><img src="https://avatars.githubusercontent.com/u/10580997?v=4?s=100" width="100px;" alt="aoshiguchen"/><br /><sub><b>aoshiguchen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/caibenxiang"><img src="https://avatars.githubusercontent.com/u/4568241?v=4?s=100" width="100px;" alt="蔡本祥"/><br /><sub><b>蔡本祥</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caibenxiang" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.fckeverything.cn:4000/"><img src="https://avatars.githubusercontent.com/u/13827124?v=4?s=100" width="100px;" alt="浮游"/><br /><sub><b>浮游</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lifefloating" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Grass-Life"><img src="https://avatars.githubusercontent.com/u/114381513?v=4?s=100" width="100px;" alt="Grass-Life"/><br /><sub><b>Grass-Life</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Grass-Life" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaohe428"><img src="https://avatars.githubusercontent.com/u/99130317?v=4?s=100" width="100px;" alt="xiaohe428"/><br /><sub><b>xiaohe428</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/baiban114"><img src="https://avatars.githubusercontent.com/u/59152619?v=4?s=100" width="100px;" alt="TableRow"/><br /><sub><b>TableRow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ByteIDance"><img src="https://avatars.githubusercontent.com/u/100207562?v=4?s=100" width="100px;" alt="ByteIDance"/><br /><sub><b>ByteIDance</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ByteIDance" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/mangel2002"><img src="https://avatars.githubusercontent.com/u/9348020?v=4?s=100" width="100px;" alt="Jangfe"/><br /><sub><b>Jangfe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mangel2002" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zqr10159"><img src="https://avatars.githubusercontent.com/u/30048352?v=4?s=100" width="100px;" alt="zqr10159"/><br /><sub><b>zqr10159</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Code">💻</a> <a href="#blog-zqr10159" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azqr10159" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Tests">⚠️</a> <a href="#design-zqr10159" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/vinci-897"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="vinci"/><br /><sub><b>vinci</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Documentation">📖</a> <a href="#design-vinci-897" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/js110"><img src="https://avatars.githubusercontent.com/u/51191863?v=4?s=100" width="100px;" alt="js110"/><br /><sub><b>js110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=js110" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/JavaLionLi"><img src="https://avatars.githubusercontent.com/u/31852897?v=4?s=100" width="100px;" alt="CrazyLionLi"/><br /><sub><b>CrazyLionLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaLionLi" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.banmajio.com"><img src="https://avatars.githubusercontent.com/u/53471385?v=4?s=100" width="100px;" alt="banmajio"/><br /><sub><b>banmajio</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=banmajio" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://suder.fun"><img src="https://avatars.githubusercontent.com/u/69955165?v=4?s=100" width="100px;" alt="topsuder"/><br /><sub><b>topsuder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=topsuder" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/richar2022"><img src="https://avatars.githubusercontent.com/u/129016397?v=4?s=100" width="100px;" alt="richar2022"/><br /><sub><b>richar2022</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=richar2022" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/fcb-xiaobo"><img src="https://avatars.githubusercontent.com/u/60566194?v=4?s=100" width="100px;" alt="fcb-xiaobo"/><br /><sub><b>fcb-xiaobo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fcb-xiaobo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wenkyzhang"><img src="https://avatars.githubusercontent.com/u/13983669?v=4?s=100" width="100px;" alt="wenkyzhang"/><br /><sub><b>wenkyzhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wenkyzhang" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ZangJuxy"><img src="https://avatars.githubusercontent.com/u/71380295?v=4?s=100" width="100px;" alt="ZangJuxy"/><br /><sub><b>ZangJuxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZangJuxy" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/l646505418"><img src="https://avatars.githubusercontent.com/u/50475131?v=4?s=100" width="100px;" alt="l646505418"/><br /><sub><b>l646505418</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=l646505418" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Al646505418" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.carpewang.com"><img src="https://avatars.githubusercontent.com/u/78642589?v=4?s=100" width="100px;" alt="Carpe-Wang"/><br /><sub><b>Carpe-Wang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Carpe-Wang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACarpe-Wang" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/moshu023"><img src="https://avatars.githubusercontent.com/u/48593205?v=4?s=100" width="100px;" alt="莫枢"/><br /><sub><b>莫枢</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moshu023" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/huangcanda"><img src="https://avatars.githubusercontent.com/u/4470566?v=4?s=100" width="100px;" alt="huangcanda"/><br /><sub><b>huangcanda</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=huangcanda" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.zrkizzy.com"><img src="https://avatars.githubusercontent.com/u/85340613?v=4?s=100" width="100px;" alt="世纪末的架构师"/><br /><sub><b>世纪末的架构师</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Architect-Java" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ShuningWan"><img src="https://avatars.githubusercontent.com/u/31086770?v=4?s=100" width="100px;" alt="ShuningWan"/><br /><sub><b>ShuningWan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShuningWan" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MrYZhou"><img src="https://avatars.githubusercontent.com/u/44339602?v=4?s=100" width="100px;" alt="MrYZhou"/><br /><sub><b>MrYZhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrYZhou" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/suncqujsj"><img src="https://avatars.githubusercontent.com/u/8012932?v=4?s=100" width="100px;" alt="suncqujsj"/><br /><sub><b>suncqujsj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=suncqujsj" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/sunqinbo"><img src="https://avatars.githubusercontent.com/u/1428540?v=4?s=100" width="100px;" alt="sunqinbo"/><br /><sub><b>sunqinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sunqinbo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/haoww"><img src="https://avatars.githubusercontent.com/u/32739294?v=4?s=100" width="100px;" alt="haoww"/><br /><sub><b>haoww</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haoww" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/i-mayuan"><img src="https://avatars.githubusercontent.com/u/101498477?v=4?s=100" width="100px;" alt="i-mayuan"/><br /><sub><b>i-mayuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=i-mayuan" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/fengruge"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="fengruge"/><br /><sub><b>fengruge</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengruge" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/aystzh"><img src="https://avatars.githubusercontent.com/u/38125392?v=4?s=100" width="100px;" alt="zhanghuan"/><br /><sub><b>zhanghuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aystzh" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/shenyumin"><img src="https://avatars.githubusercontent.com/u/8438506?v=4?s=100" width="100px;" alt="shenymin"/><br /><sub><b>shenymin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shenyumin" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/dhruva1995"><img src="https://avatars.githubusercontent.com/u/12976351?v=4?s=100" width="100px;" alt="Dhruva Chandra"/><br /><sub><b>Dhruva Chandra</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dhruva1995" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/weiwang988"><img src="https://avatars.githubusercontent.com/u/58241726?v=4?s=100" width="100px;" alt="miss_z"/><br /><sub><b>miss_z</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weiwang988" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt990"><img src="https://avatars.githubusercontent.com/u/86013697?v=4?s=100" width="100px;" alt="wyt990"/><br /><sub><b>wyt990</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt990" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/licocon"><img src="https://avatars.githubusercontent.com/u/36863277?v=4?s=100" width="100px;" alt="licocon"/><br /><sub><b>licocon</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=licocon" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/2406450951"><img src="https://avatars.githubusercontent.com/u/48074721?v=4?s=100" width="100px;" alt="Mi Na"/><br /><sub><b>Mi Na</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=2406450951" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Kylin-Guo"><img src="https://avatars.githubusercontent.com/u/131239856?v=4?s=100" width="100px;" alt="Kylin-Guo"/><br /><sub><b>Kylin-Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Kylin-Guo" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/1797899698"><img src="https://avatars.githubusercontent.com/u/40411650?v=4?s=100" width="100px;" alt="Mr灬Dong先生"/><br /><sub><b>Mr灬Dong先生</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1797899698" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="http://neilblaze.live"><img src="https://avatars.githubusercontent.com/u/48355572?v=4?s=100" width="100px;" alt="Pratyay Banerjee"/><br /><sub><b>Pratyay Banerjee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yujianzhong520"><img src="https://avatars.githubusercontent.com/u/63705063?v=4?s=100" width="100px;" alt="yujianzhong520"/><br /><sub><b>yujianzhong520</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yujianzhong520" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://sppan24.github.io/"><img src="https://avatars.githubusercontent.com/u/15795173?v=4?s=100" width="100px;" alt="SPPan"/><br /><sub><b>SPPan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sppan24" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/1130600015"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1130600015" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/QZmp236478"><img src="https://avatars.githubusercontent.com/u/56623162?v=4?s=100" width="100px;" alt="impress"/><br /><sub><b>impress</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QZmp236478" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jx3775250"><img src="https://avatars.githubusercontent.com/u/40455946?v=4?s=100" width="100px;" alt="凌晨一点半"/><br /><sub><b>凌晨一点半</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx3775250" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/eeshaanSA"><img src="https://avatars.githubusercontent.com/u/100678386?v=4?s=100" width="100px;" alt="Eeshaan Sawant"/><br /><sub><b>Eeshaan Sawant</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eeshaanSA" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/nandofromthebando"><img src="https://avatars.githubusercontent.com/u/87321214?v=4?s=100" width="100px;" alt="nandofromthebando"/><br /><sub><b>nandofromthebando</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=nandofromthebando" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/caiboking"><img src="https://avatars.githubusercontent.com/u/6509883?v=4?s=100" width="100px;" alt="caiboking"/><br /><sub><b>caiboking</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caiboking" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/baixing99"><img src="https://avatars.githubusercontent.com/u/73473087?v=4?s=100" width="100px;" alt="baixing99"/><br /><sub><b>baixing99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baixing99" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ifrenzyc"><img src="https://avatars.githubusercontent.com/u/543927?v=4?s=100" width="100px;" alt="Yang Chuang"/><br /><sub><b>Yang Chuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ifrenzyc" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wlin20"><img src="https://avatars.githubusercontent.com/u/20657577?v=4?s=100" width="100px;" alt="wlin20"/><br /><sub><b>wlin20</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wlin20" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/guojing1983"><img src="https://avatars.githubusercontent.com/u/60596094?v=4?s=100" width="100px;" alt="guojing1983"/><br /><sub><b>guojing1983</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=guojing1983" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/itxxq"><img src="https://avatars.githubusercontent.com/u/46962357?v=4?s=100" width="100px;" alt="moxi"/><br /><sub><b>moxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=itxxq" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/qq471754603"><img src="https://avatars.githubusercontent.com/u/23146592?v=4?s=100" width="100px;" alt="qq471754603"/><br /><sub><b>qq471754603</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qq471754603" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/san346596324"><img src="https://avatars.githubusercontent.com/u/30828520?v=4?s=100" width="100px;" alt="渭雨"/><br /><sub><b>渭雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=san346596324" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/luoxuanzao"><img src="https://avatars.githubusercontent.com/u/44692579?v=4?s=100" width="100px;" alt="liuxuezhuo"/><br /><sub><b>liuxuezhuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luoxuanzao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ChenXiangxxxxx"><img src="https://avatars.githubusercontent.com/u/90089594?v=4?s=100" width="100px;" alt="ChenXiangxxxxx"/><br /><sub><b>ChenXiangxxxxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChenXiangxxxxx" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Mr-zhou315"><img src="https://avatars.githubusercontent.com/u/10276100?v=4?s=100" width="100px;" alt="Mr.zhou"/><br /><sub><b>Mr.zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Mr-zhou315" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/XimfengYao"><img src="https://avatars.githubusercontent.com/u/17541537?v=4?s=100" width="100px;" alt="姚贤丰"/><br /><sub><b>姚贤丰</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XimfengYao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LINGLUOJUN"><img src="https://avatars.githubusercontent.com/u/16778977?v=4?s=100" width="100px;" alt="lingluojun"/><br /><sub><b>lingluojun</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LINGLUOJUN" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.luelueking.com"><img src="https://avatars.githubusercontent.com/u/93204032?v=4?s=100" width="100px;" alt="1ue"/><br /><sub><b>1ue</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luelueking" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.jimmyqiao.top"><img src="https://avatars.githubusercontent.com/u/67301054?v=4?s=100" width="100px;" alt="qyaaaa"/><br /><sub><b>qyaaaa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qyaaaa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aqyaaaa" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://novohit.top"><img src="https://avatars.githubusercontent.com/u/101090395?v=4?s=100" width="100px;" alt="novohit"/><br /><sub><b>novohit</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=novohit" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/rbsrcy"><img src="https://avatars.githubusercontent.com/u/4798540?v=4?s=100" width="100px;" alt="zhuoshangyi"/><br /><sub><b>zhuoshangyi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=rbsrcy" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ruanliang-hualun"><img src="https://avatars.githubusercontent.com/u/65543716?v=4?s=100" width="100px;" alt="ruanliang"/><br /><sub><b>ruanliang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Eden4701"><img src="https://avatars.githubusercontent.com/u/68422437?v=4?s=100" width="100px;" alt="Eden4701"/><br /><sub><b>Eden4701</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Documentation">📖</a> <a href="#design-Eden4701" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/XiaTian688"><img src="https://avatars.githubusercontent.com/u/111830921?v=4?s=100" width="100px;" alt="XiaTian688"/><br /><sub><b>XiaTian688</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XiaTian688" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/liyin"><img src="https://avatars.githubusercontent.com/u/863169?v=4?s=100" width="100px;" alt="liyinjiang"/><br /><sub><b>liyinjiang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liyin" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jiashu1024"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiashu1024" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ZY945"><img src="https://avatars.githubusercontent.com/u/74083801?v=4?s=100" width="100px;" alt="东风"/><br /><sub><b>东风</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Code">💻</a> <a href="#design-ZY945" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AZY945" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/prolevel1"><img src="https://avatars.githubusercontent.com/u/51995525?v=4?s=100" width="100px;" alt="sonam singh"/><br /><sub><b>sonam singh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=prolevel1" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ZhangZixuan1994"><img src="https://avatars.githubusercontent.com/u/20011653?v=4?s=100" width="100px;" alt="ZhangZixuan1994"/><br /><sub><b>ZhangZixuan1994</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhangZixuan1994" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/hurenjie1"><img src="https://avatars.githubusercontent.com/u/40120355?v=4?s=100" width="100px;" alt="SHIG"/><br /><sub><b>SHIG</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hurenjie1" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://tslj1024.github.io/"><img src="https://avatars.githubusercontent.com/u/155222677?v=4?s=100" width="100px;" alt="泰上老菌"/><br /><sub><b>泰上老菌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tslj1024" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ldysdu"><img src="https://avatars.githubusercontent.com/u/15815338?v=4?s=100" width="100px;" alt="ldysdu"/><br /><sub><b>ldysdu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldysdu" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/GEM0816g"><img src="https://avatars.githubusercontent.com/u/85116017?v=4?s=100" width="100px;" alt="梁同学"/><br /><sub><b>梁同学</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=GEM0816g" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/avvCode"><img src="https://avatars.githubusercontent.com/u/113538532?v=4?s=100" width="100px;" alt="avv"/><br /><sub><b>avv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=avvCode" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yqxxgh"><img src="https://avatars.githubusercontent.com/u/42080876?v=4?s=100" width="100px;" alt="yqxxgh"/><br /><sub><b>yqxxgh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayqxxgh" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieShi46"><img src="https://avatars.githubusercontent.com/u/149798885?v=4?s=100" width="100px;" alt="CharlieShi46"/><br /><sub><b>CharlieShi46</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieShi46" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Nctllnty"><img src="https://avatars.githubusercontent.com/u/33241818?v=4?s=100" width="100px;" alt="Nctllnty"/><br /><sub><b>Nctllnty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Nctllnty" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Wang-Yonghao"><img src="https://avatars.githubusercontent.com/u/48146606?v=4?s=100" width="100px;" alt="Wang-Yonghao"/><br /><sub><b>Wang-Yonghao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Wang-Yonghao" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.yuque.com/dudiao/yy"><img src="https://avatars.githubusercontent.com/u/38355949?v=4?s=100" width="100px;" alt="读钓"/><br /><sub><b>读钓</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dudiao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/starmilkxin"><img src="https://avatars.githubusercontent.com/u/55646681?v=4?s=100" width="100px;" alt="Xin"/><br /><sub><b>Xin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starmilkxin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Astarmilkxin" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/handy-git"><img src="https://avatars.githubusercontent.com/u/32837980?v=4?s=100" width="100px;" alt="handy"/><br /><sub><b>handy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=handy-git" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LiuTianyou"><img src="https://avatars.githubusercontent.com/u/30208283?v=4?s=100" width="100px;" alt="LiuTianyou"/><br /><sub><b>LiuTianyou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALiuTianyou" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Tests">⚠️</a> <a href="#blog-LiuTianyou" title="Blogposts">📝</a> <a href="#design-LiuTianyou" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/WinterKi1ler"><img src="https://avatars.githubusercontent.com/u/160592092?v=4?s=100" width="100px;" alt="WinterKi1ler"/><br /><sub><b>WinterKi1ler</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=WinterKi1ler" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://sharehoo.cn/"><img src="https://avatars.githubusercontent.com/u/45377370?v=4?s=100" width="100px;" alt="miki"/><br /><sub><b>miki</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miki-hmt" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://codeflex.substack.com/"><img src="https://avatars.githubusercontent.com/u/85513042?v=4?s=100" width="100px;" alt="Keshav Carpenter"/><br /><sub><b>Keshav Carpenter</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/makechoicenow"><img src="https://avatars.githubusercontent.com/u/9911918?v=4?s=100" width="100px;" alt="makechoicenow"/><br /><sub><b>makechoicenow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=makechoicenow" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/gjjjj0101"><img src="https://avatars.githubusercontent.com/u/71874373?v=4?s=100" width="100px;" alt="Gao Jian"/><br /><sub><b>Gao Jian</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Documentation">📖</a> <a href="#design-gjjjj0101" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agjjjj0101" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://jangto.tistory.com/"><img src="https://avatars.githubusercontent.com/u/37864182?v=4?s=100" width="100px;" alt="Hyeon Sung"/><br /><sub><b>Hyeon Sung</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://crossoverjie.top/"><img src="https://avatars.githubusercontent.com/u/15684156?v=4?s=100" width="100px;" alt="crossoverJie"/><br /><sub><b>crossoverJie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Documentation">📖</a> <a href="#blog-crossoverJie" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Tests">⚠️</a> <a href="#design-crossoverJie" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/PeixyJ"><img src="https://avatars.githubusercontent.com/u/45998593?v=4?s=100" width="100px;" alt="PeixyJ"/><br /><sub><b>PeixyJ</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PeixyJ" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Hi-Mr-Wind"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="风如歌"/><br /><sub><b>风如歌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Hi-Mr-Wind" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MananPoojara"><img src="https://avatars.githubusercontent.com/u/104253184?v=4?s=100" width="100px;" alt="Manan Pujara"/><br /><sub><b>Manan Pujara</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MananPoojara" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xuziyang"><img src="https://avatars.githubusercontent.com/u/8465969?v=4?s=100" width="100px;" alt="xuziyang"/><br /><sub><b>xuziyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Axuziyang" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lwqzz"><img src="https://avatars.githubusercontent.com/u/62584513?v=4?s=100" width="100px;" alt="lwqzz"/><br /><sub><b>lwqzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwqzz" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/YxYL6125"><img src="https://avatars.githubusercontent.com/u/91076160?v=4?s=100" width="100px;" alt="YxYL"/><br /><sub><b>YxYL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YxYL6125" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/tomorrowshipyltm"><img src="https://avatars.githubusercontent.com/u/61336903?v=4?s=100" width="100px;" alt="tomorrowshipyltm"/><br /><sub><b>tomorrowshipyltm</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomorrowshipyltm" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/15613060203"><img src="https://avatars.githubusercontent.com/u/41351615?v=4?s=100" width="100px;" alt="栗磊"/><br /><sub><b>栗磊</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=15613060203" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Alanxtl"><img src="https://avatars.githubusercontent.com/u/25652981?v=4?s=100" width="100px;" alt="Alan"/><br /><sub><b>Alan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Alanxtl" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.hadoop.wiki/"><img src="https://avatars.githubusercontent.com/u/29418975?v=4?s=100" width="100px;" alt="Jast"/><br /><sub><b>Jast</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Code">💻</a> <a href="#ideas-zhangshenghang" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Documentation">📖</a> <a href="#blog-zhangshenghang" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azhangshenghang" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Tests">⚠️</a> <a href="#design-zhangshenghang" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/zuobiao-zhou"><img src="https://avatars.githubusercontent.com/u/61108539?v=4?s=100" width="100px;" alt="Zhang Yuxuan"/><br /><sub><b>Zhang Yuxuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azuobiao-zhou" title="Bug reports">🐛</a> <a href="#blog-zuobiao-zhou" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Pzz-2021"><img src="https://avatars.githubusercontent.com/u/118056735?v=4?s=100" width="100px;" alt="P.P."/><br /><sub><b>P.P.</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Pzz-2021" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LLP2333"><img src="https://avatars.githubusercontent.com/u/61670545?v=4?s=100" width="100px;" alt="llp2333"/><br /><sub><b>llp2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LLP2333" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/HeartLinked"><img src="https://avatars.githubusercontent.com/u/78212101?v=4?s=100" width="100px;" alt="feiyang li"/><br /><sub><b>feiyang li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HeartLinked" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Aias00"><img src="https://avatars.githubusercontent.com/u/25810623?v=4?s=100" width="100px;" alt="aias00"/><br /><sub><b>aias00</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AAias00" title="Bug reports">🐛</a> <a href="#ideas-Aias00" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/westboy"><img src="https://avatars.githubusercontent.com/u/6385565?v=4?s=100" width="100px;" alt="Jin"/><br /><sub><b>Jin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=westboy" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://blog.csdn.net/qq_52397471"><img src="https://avatars.githubusercontent.com/u/77964041?v=4?s=100" width="100px;" alt="YuLuo"/><br /><sub><b>YuLuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayuluo-yx" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Tests">⚠️</a> <a href="#blog-yuluo-yx" title="Blogposts">📝</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Yanshuming1"><img src="https://avatars.githubusercontent.com/u/118667222?v=4?s=100" width="100px;" alt="linDong"/><br /><sub><b>linDong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AYanshuming1" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lwjxy"><img src="https://avatars.githubusercontent.com/u/52726400?v=4?s=100" width="100px;" alt="lwjxy"/><br /><sub><b>lwjxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwjxy" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://thespica.github.io/"><img src="https://avatars.githubusercontent.com/u/119573640?v=4?s=100" width="100px;" alt="John"/><br /><sub><b>John</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/boatrainlsz"><img src="https://avatars.githubusercontent.com/u/18243785?v=4?s=100" width="100px;" alt="boatrainlsz"/><br /><sub><b>boatrainlsz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boatrainlsz" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://www.yitianyigexiangfa.com/"><img src="https://avatars.githubusercontent.com/u/3973419?v=4?s=100" width="100px;" alt="Bill Lau"/><br /><sub><b>Bill Lau</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaProgrammerLB" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lw-yang"><img src="https://avatars.githubusercontent.com/u/23456873?v=4?s=100" width="100px;" alt="lwyang"/><br /><sub><b>lwyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lw-yang" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xfl12345"><img src="https://avatars.githubusercontent.com/u/17960863?v=4?s=100" width="100px;" alt="xfl12345"/><br /><sub><b>xfl12345</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xfl12345" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yykaue"><img src="https://avatars.githubusercontent.com/u/22905143?v=4?s=100" width="100px;" alt="Limbo"/><br /><sub><b>Limbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yykaue" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/irenhongyan"><img src="https://avatars.githubusercontent.com/u/53438321?v=4?s=100" width="100px;" alt="哈哈哈哈哈哈哈哈哈"/><br /><sub><b>哈哈哈哈哈哈哈哈哈</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=irenhongyan" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ileonli"><img src="https://avatars.githubusercontent.com/u/45332412?v=4?s=100" width="100px;" alt="Leon Li"/><br /><sub><b>Leon Li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ileonli" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://fnil.net/"><img src="https://avatars.githubusercontent.com/u/14142?v=4?s=100" width="100px;" alt="dennis zhuang"/><br /><sub><b>dennis zhuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=killme2008" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/kerwin612"><img src="https://avatars.githubusercontent.com/u/3371163?v=4?s=100" width="100px;" alt="Kerwin Bryant"/><br /><sub><b>Kerwin Bryant</b></sub></a><br /><a href="#design-kerwin612" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akerwin612" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ShineDevelopment"><img src="https://avatars.githubusercontent.com/u/59306780?v=4?s=100" width="100px;" alt="daixianglong"/><br /><sub><b>daixianglong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShineDevelopment" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/mchgood"><img src="https://avatars.githubusercontent.com/u/38482005?v=4?s=100" width="100px;" alt="mchgood"/><br /><sub><b>mchgood</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mchgood" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/pwallk"><img src="https://avatars.githubusercontent.com/u/69385076?v=4?s=100" width="100px;" alt="kangli"/><br /><sub><b>kangli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apwallk" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/cdphantom"><img src="https://avatars.githubusercontent.com/u/12674795?v=4?s=100" width="100px;" alt="cdphantom"/><br /><sub><b>cdphantom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cdphantom" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/asd108908382"><img src="https://avatars.githubusercontent.com/u/77717999?v=4?s=100" width="100px;" alt="jiawei.guo"/><br /><sub><b>jiawei.guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=asd108908382" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/QBH-insist"><img src="https://avatars.githubusercontent.com/u/39401478?v=4?s=100" width="100px;" alt="QBH-insist"/><br /><sub><b>QBH-insist</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QBH-insist" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jiangsh-ui"><img src="https://avatars.githubusercontent.com/u/86990361?v=4?s=100" width="100px;" alt="jiangsh"/><br /><sub><b>jiangsh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiangsh-ui" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/keaifafafa"><img src="https://avatars.githubusercontent.com/u/83876361?v=4?s=100" width="100px;" alt="Keaifa"/><br /><sub><b>Keaifa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=keaifafafa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akeaifafafa" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/loong95"><img src="https://avatars.githubusercontent.com/u/16333958?v=4?s=100" width="100px;" alt="Loong"/><br /><sub><b>Loong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=loong95" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ceekay47"><img src="https://avatars.githubusercontent.com/u/104664857?v=4?s=100" width="100px;" alt="Chandrakant Vankayalapati"/><br /><sub><b>Chandrakant Vankayalapati</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ceekay47" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MRgenial"><img src="https://avatars.githubusercontent.com/u/49973336?v=4?s=100" width="100px;" alt="b_mountain"/><br /><sub><b>b_mountain</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MRgenial" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/TemirlanBasitov"><img src="https://avatars.githubusercontent.com/u/57500808?v=4?s=100" width="100px;" alt="TemirlanBasitov"/><br /><sub><b>TemirlanBasitov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TemirlanBasitov" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wyfvsfy"><img src="https://avatars.githubusercontent.com/u/11973517?v=4?s=100" width="100px;" alt="wyfvsfy"/><br /><sub><b>wyfvsfy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyfvsfy" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/sherry-peng2333"><img src="https://avatars.githubusercontent.com/u/70619577?v=4?s=100" width="100px;" alt="sherry-peng2333"/><br /><sub><b>sherry-peng2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sherry-peng2333" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lixiaobaivv"><img src="https://avatars.githubusercontent.com/u/39290771?v=4?s=100" width="100px;" alt="Yzzz"/><br /><sub><b>Yzzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lixiaobaivv" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.bckf.cn/"><img src="https://avatars.githubusercontent.com/u/13309008?v=4?s=100" width="100px;" alt="puruidong"/><br /><sub><b>puruidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pruidong" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/shinestare"><img src="https://avatars.githubusercontent.com/u/13570619?v=4?s=100" width="100px;" alt="shinestare"/><br /><sub><b>shinestare</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shinestare" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/po-168"><img src="https://avatars.githubusercontent.com/u/185745593?v=4?s=100" width="100px;" alt="po-168"/><br /><sub><b>po-168</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=po-168" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/All-The-Best-for"><img src="https://avatars.githubusercontent.com/u/76414672?v=4?s=100" width="100px;" alt="wbs99"/><br /><sub><b>wbs99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=All-The-Best-for" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/starryCoder"><img src="https://avatars.githubusercontent.com/u/46510059?v=4?s=100" width="100px;" alt="starryCoder"/><br /><sub><b>starryCoder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starryCoder" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/hasimmollah"><img src="https://avatars.githubusercontent.com/u/32538599?v=4?s=100" width="100px;" alt="hasimmollah"/><br /><sub><b>hasimmollah</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hasimmollah" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ayu-v0"><img src="https://avatars.githubusercontent.com/u/127600988?v=4?s=100" width="100px;" alt="Ayu"/><br /><sub><b>Ayu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ayu-v0" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Rancho-7"><img src="https://avatars.githubusercontent.com/u/59016860?v=4?s=100" width="100px;" alt="Nick Guo"/><br /><sub><b>Nick Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ARancho-7" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/doveLin0818"><img src="https://avatars.githubusercontent.com/u/190927907?v=4?s=100" width="100px;" alt="doveLin"/><br /><sub><b>doveLin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=doveLin0818" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://zzrl.cc/"><img src="https://avatars.githubusercontent.com/u/91836599?v=4?s=100" width="100px;" alt="yunfan24"/><br /><sub><b>yunfan24</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayunfan24" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Tests">⚠️</a> <a href="#blog-yunfan24" title="Blogposts">📝</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lctking"><img src="https://avatars.githubusercontent.com/u/168249998?v=4?s=100" width="100px;" alt="nullwli"/><br /><sub><b>nullwli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lctking" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://simonsigre.com/"><img src="https://avatars.githubusercontent.com/u/14932913?v=4?s=100" width="100px;" alt="Simon Sigré"/><br /><sub><b>Simon Sigré</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://www.ponfee.cn/"><img src="https://avatars.githubusercontent.com/u/46117331?v=4?s=100" width="100px;" alt="ponfee"/><br /><sub><b>ponfee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ponfee" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Vedant7789"><img src="https://avatars.githubusercontent.com/u/147625492?v=4?s=100" width="100px;" alt="Vedant7789"/><br /><sub><b>Vedant7789</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Vedant7789" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Craaaaazy77"><img src="https://avatars.githubusercontent.com/u/23025522?v=4?s=100" width="100px;" alt="Craaaaazy77"/><br /><sub><b>Craaaaazy77</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Craaaaazy77" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Suvrat1629"><img src="https://avatars.githubusercontent.com/u/140749446?v=4?s=100" width="100px;" alt="Suvrat1629"/><br /><sub><b>Suvrat1629</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Suvrat1629" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="http://ghyghoo8.github.io/"><img src="https://avatars.githubusercontent.com/u/363129?v=4?s=100" width="100px;" alt="ghy"/><br /><sub><b>ghy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ghyghoo8" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/helei1030"><img src="https://avatars.githubusercontent.com/u/11839080?v=4?s=100" width="100px;" alt="helei1030"/><br /><sub><b>helei1030</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=helei1030" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://medium.com/@pjfanning"><img src="https://avatars.githubusercontent.com/u/11783444?v=4?s=100" width="100px;" alt="PJ Fanning"/><br /><sub><b>PJ Fanning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apjfanning" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MonsterChenzhuo"><img src="https://avatars.githubusercontent.com/u/60029759?v=4?s=100" width="100px;" alt="monster"/><br /><sub><b>monster</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MonsterChenzhuo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/MasamiYui"><img src="https://avatars.githubusercontent.com/u/22274133?v=4?s=100" width="100px;" alt="Sherlock Yin"/><br /><sub><b>Sherlock Yin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AMasamiYui" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/wanhao23"><img src="https://avatars.githubusercontent.com/u/29560961?v=4?s=100" width="100px;" alt="wanhao"/><br /><sub><b>wanhao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jonasHanhan"><img src="https://avatars.githubusercontent.com/u/130035609?v=4?s=100" width="100px;" alt="jonasHanhan"/><br /><sub><b>jonasHanhan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jonasHanhan" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/NikhilMurugesan"><img src="https://avatars.githubusercontent.com/u/49281792?v=4?s=100" width="100px;" alt="NikhilMurugesan"/><br /><sub><b>NikhilMurugesan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NikhilMurugesan" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/myangle1120"><img src="https://avatars.githubusercontent.com/u/19237013?v=4?s=100" width="100px;" alt="myangle1120"/><br /><sub><b>myangle1120</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=myangle1120" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Anotbugggg" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Documentation">📖</a> <a href="#ideas-JuJinPark" title="Ideas, Planning, & Feedback">🤔</a> <a href="#blog-JuJinPark" title="Blogposts">📝</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALL-LIN" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abigcyy" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td> + </tr> + <tr> + <td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td> + <td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td> + </tr> + </tbody> +</table> + +<!-- markdownlint-restore --> +<!-- prettier-ignore-end --> + +<!-- ALL-CONTRIBUTORS-LIST:END --> + +## 💬 コミュニティ交流 + +##### チャネル + +[メール](https://lists.apache.org/list.html?dev@hertzbeat.apache.org) : メーリングリストに登録するために、```dev-subscribe@hertzbeat.apache.org```にメールを送ってください。 + +[Chat On Discord](https://discord.gg/Fb6M73htGr) + +WeChatグループ : `ahertzbeat` を検索. + +WeChat公式アカウント : `usthecom`を検索. + +[Github Discussion](https://github.com/apache/hertzbeat/discussions) + +[Follow Us Twitter](https://x.com/hertzbeat1024) + +[Subscribe YouTube](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow) + + +##### Open-Source Project Build From Open-Source + +HertzBeat is built on so many great open source projects, thanks to them! + +- `Java Spring SpringBoot Jpa Maven Assembly Netty Lombok Sureness Protobuf HttpClient Guava SnakeYaml JsonPath ...` +- `TypeScript Angular NG-ZORRO NG-ALAIN NodeJs Npm Html Less Echarts Rxjs ZoneJs MonacoEditor SlickCarousel Docusaurus ...` + + +## Landscape + +<p align="left"> +<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300">  <img src="./home/static/img/home/cncf-right-logo.svg" width="345" /> +<br /><br /> +HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat"> +CNCF Observability And Analysis - Observability Landscape.</a> +</p> + +## 🛡️ License +[`Apache License, Version 2.0`](https://www.apache.org/licenses/LICENSE-2.0.html) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..eae5a21 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +Apache HertzBeat™ follows the [ASF vulnerability handling process](https://apache.org/security/#vulnerability-handling) + +## Supported Versions + +| Version | Supported | +|-------------------|--------------------| +| Latest Version | :white_check_mark: | +| Any Other Version | :x: | + +## Reporting a Vulnerability + +Please do not file GitHub issues for security vulnerabilities as they are public! + +To report a new vulnerability you have discovered please follow the [ASF vulnerability reporting process](https://apache.org/security/#reporting-a-vulnerability). + +## Extra Information + +- [Apache HertzBeat™ Security Model](https://hertzbeat.apache.org/docs/help/security_model) +- [ASF Security](https://www.apache.org/security/) +- [Apache HertzBeat™ Document](https://hertzbeat.apache.org/docs/) diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000..d372428 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,35 @@ +Please add the corresponding e2e (aka end-to-end) test cases if you add or update APIs. + +## How to work +* Start and watch the [Docker Compose](https://docs.docker.com/compose/) environment + * The Compose file is `e2e/docker-compose.yml` + * The `testing` container uses [api-testing](https://github.com/LinuxSuRen/api-testing) + * The main test definition file is `e2e/testsuite.yaml` + * Before starting the suite locally, build the `apache/hertzbeat:test` image from the repository root: + + ```bash + mvn clean -B package -Prelease -Dmaven.test.skip=true --file pom.xml + docker build -t apache/hertzbeat:test -f script/docker/server/Dockerfile ./dist + ``` + +* Run the E2E tests via [api-testing](https://github.com/LinuxSuRen/api-testing) + * The test cases run from top to bottom + * You can add the necessary assertions there + * Test data files are under `e2e/data/` + * The test report is generated at `e2e/report/report.md` + * HertzBeat logs are written to `e2e/logs/` + +## Run locally +Please follow these steps if you want to run the E2E tests locally. + +> Please make sure that Docker Compose v2 is installed + +* Build the local `apache/hertzbeat:test` image from the repository root +* Change the directory to `e2e`, then run: + + ```bash + docker compose up --exit-code-from testing --remove-orphans + ``` + +* If your environment uses the standalone Compose binary, use `docker-compose` instead of `docker compose` +* After the run, check `e2e/report/report.md` and `e2e/logs/` for the test result and runtime logs diff --git a/e2e/data/monitor-ftp.json b/e2e/data/monitor-ftp.json new file mode 100644 index 0000000..f8301e4 --- /dev/null +++ b/e2e/data/monitor-ftp.json @@ -0,0 +1,46 @@ +{ + "detected": false, + "collector": "", + "monitor": { + "intervals": 60, + "tags": [], + "app": "ftp", + "instance": "127.0.0.1", + "name": "{{.param.monitorFTP}}" + }, + "params": [ + { + "field": "host", + "type": 1, + "paramValue": "127.0.0.1" + }, + { + "field": "port", + "type": 0, + "paramValue": 21 + }, + { + "field": "direction", + "type": 1, + "paramValue": "127.0.0.1" + }, + { + "field": "timeout", + "type": 0, + "paramValue": 1000 + }, + { + "field": "ssl", + "type": 1, + "paramValue": false + }, + { + "field": "username", + "type": 1 + }, + { + "field": "password", + "type": 1 + } + ] +} diff --git a/e2e/data/monitor-http.json b/e2e/data/monitor-http.json new file mode 100644 index 0000000..f1983ea --- /dev/null +++ b/e2e/data/monitor-http.json @@ -0,0 +1,83 @@ +{ + "detected": false, + "collector": "", + "monitor": { + "intervals": 60, + "tags": [], + "app": "api", + "instance": "127.0.0.1", + "name": "{{.param.monitorHTTP}}" + }, + "params": [ + { + "field": "host", + "type": 1, + "paramValue": "127.0.0.1" + }, + { + "field": "port", + "type": 0, + "paramValue": 80 + }, + { + "field": "httpMethod", + "type": 1, + "paramValue": "GET" + }, + { + "field": "uri", + "type": 1 + }, + { + "field": "ssl", + "type": 1, + "paramValue": false + }, + { + "field": "headers", + "type": 3 + }, + { + "field": "params", + "type": 3 + }, + { + "field": "timeout", + "type": 0 + }, + { + "field": "contentType", + "type": 1 + }, + { + "field": "payload", + "type": 1 + }, + { + "field": "authType", + "type": 1 + }, + { + "field": "username", + "type": 1 + }, + { + "field": "password", + "type": 1 + }, + { + "field": "keyword", + "type": 1 + }, + { + "field": "successCode", + "type": 4, + "paramValue": "200, 201" + }, + { + "field": "enableUrlEncoding", + "type": 1, + "paramValue": true + } + ] +} \ No newline at end of file diff --git a/e2e/data/monitor-ping.json b/e2e/data/monitor-ping.json new file mode 100644 index 0000000..6df9e15 --- /dev/null +++ b/e2e/data/monitor-ping.json @@ -0,0 +1,23 @@ +{ + "detected": false, + "collector": "", + "monitor": { + "intervals": 60, + "tags": [], + "app": "ping", + "instance": "127.0.0.1", + "name": "{{.param.monitorPing}}" + }, + "params": [ + { + "field": "host", + "type": 1, + "paramValue": "127.0.0.1" + }, + { + "field": "timeout", + "type": 0, + "paramValue": 6000 + } + ] +} diff --git a/e2e/data/monitor-port.json b/e2e/data/monitor-port.json new file mode 100644 index 0000000..bd0ef2b --- /dev/null +++ b/e2e/data/monitor-port.json @@ -0,0 +1,28 @@ +{ + "detected": false, + "collector": "", + "monitor": { + "intervals": 60, + "tags": [], + "app": "port", + "instance": "127.0.0.1", + "name": "{{.param.monitorPort}}" + }, + "params": [ + { + "field": "host", + "type": 1, + "paramValue": "127.0.0.1" + }, + { + "field": "port", + "type": 0, + "paramValue": 80 + }, + { + "field": "timeout", + "type": 0, + "paramValue": 6000 + } + ] +} diff --git a/e2e/data/monitor-sitemap.json b/e2e/data/monitor-sitemap.json new file mode 100644 index 0000000..5146c35 --- /dev/null +++ b/e2e/data/monitor-sitemap.json @@ -0,0 +1,33 @@ +{ + "detected": false, + "collector": "", + "monitor": { + "intervals": 60, + "tags": [], + "app": "fullsite", + "instance": "127.0.0.1", + "name": "{{.param.monitorSiteMap}}" + }, + "params": [ + { + "field": "host", + "type": 1, + "paramValue": "127.0.0.1" + }, + { + "field": "port", + "type": 0, + "paramValue": 80 + }, + { + "field": "sitemap", + "type": 1, + "paramValue": "sitemap.xml" + }, + { + "field": "ssl", + "type": 1, + "paramValue": false + } + ] +} diff --git a/e2e/data/monitor-ssl.json b/e2e/data/monitor-ssl.json new file mode 100644 index 0000000..af7459d --- /dev/null +++ b/e2e/data/monitor-ssl.json @@ -0,0 +1,27 @@ +{ + "detected": false, + "collector": "", + "monitor": { + "intervals": 60, + "tags": [], + "app": "ssl_cert", + "instance": "127.0.0.1", + "name": "{{.param.monitorSSL}}" + }, + "params": [ + { + "field": "host", + "type": 1, + "paramValue": "127.0.0.1" + }, + { + "field": "port", + "type": 0, + "paramValue": 443 + }, + { + "field": "uri", + "type": 1 + } + ] +} diff --git a/e2e/data/monitor-udp.json b/e2e/data/monitor-udp.json new file mode 100644 index 0000000..636db4d --- /dev/null +++ b/e2e/data/monitor-udp.json @@ -0,0 +1,32 @@ +{ + "detected": false, + "collector": "", + "monitor": { + "intervals": 60, + "tags": [], + "app": "udp_port", + "instance": "127.0.0.1", + "name": "{{.param.monitorUDP}}" + }, + "params": [ + { + "field": "host", + "type": 1, + "paramValue": "127.0.0.1" + }, + { + "field": "port", + "type": 0, + "paramValue": 81 + }, + { + "field": "timeout", + "type": 0, + "paramValue": 6000 + }, + { + "field": "content", + "type": 1 + } + ] +} diff --git a/e2e/data/monitor-website.json b/e2e/data/monitor-website.json new file mode 100644 index 0000000..cf0ca20 --- /dev/null +++ b/e2e/data/monitor-website.json @@ -0,0 +1,52 @@ +{ + "detected": false, + "collector": "", + "monitor": { + "intervals": 60, + "tags": [], + "app": "website", + "instance": "127.0.0.1", + "name": "{{.param.monitorWebsite}}" + }, + "params": [ + { + "field": "host", + "type": 1, + "paramValue": "127.0.0.1" + }, + { + "field": "port", + "type": 0, + "paramValue": 80 + }, + { + "field": "uri", + "paramValue": 1 + }, + { + "field": "ssl", + "type": 1, + "paramValue": false + }, + { + "field": "timeout", + "type": 0 + }, + { + "field": "authType", + "type": 1 + }, + { + "field": "username", + "type": 1 + }, + { + "field": "password", + "type": 1 + }, + { + "field": "keyword", + "type": 1 + } + ] +} diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml new file mode 100644 index 0000000..f0d9c57 --- /dev/null +++ b/e2e/docker-compose.yml @@ -0,0 +1,56 @@ +# 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. + +version: '3.8' +services: + testing: + image: ghcr.io/linuxsuren/api-testing:v0.0.19 + environment: + SERVER: http://hertzbeat:1157 + container_name: e2e-testing + volumes: + - ./data/:/work/data/ + - ./testsuite.yaml:/work/testsuite.yaml + - ./report/report.md:/report.md + command: atest run -p /work/testsuite.yaml --level debug --thread 3 --report md --report-file /report.md + depends_on: + checker: + condition: service_healthy + links: + - hertzbeat + - checker + checker: + image: alpine:latest + container_name: e2e-checker + command: sh -c "apk add --update curl && tail -f /dev/null" + healthcheck: + test: [ "CMD-SHELL", + "curl -s -X POST -H \"Content-Type: application/json\" -d '{\"type\": 0,\"identifier\": \"admin\",\"credential\": \"hertzbeat\"}' http://hertzbeat:1157/api/account/auth/form | grep -q token" + ] + interval: 3s + timeout: 60s + retries: 10 + start_period: 10s + hertzbeat: + image: apache/hertzbeat:test + container_name: e2e-hertzbeat + volumes: + - ./logs/:/opt/hertzbeat/logs/ + ports: + - "1157:1157" + environment: + ALLOW_NONE_AUTHENTICATION: "yes" diff --git a/e2e/k8s/data/config/k3s.yaml b/e2e/k8s/data/config/k3s.yaml new file mode 100644 index 0000000..1268c07 --- /dev/null +++ b/e2e/k8s/data/config/k3s.yaml @@ -0,0 +1,39 @@ +# 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. + +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkakNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTkRVeU5ESTROamd3SGhjTk1qVXdOREl4TVRNME1UQTRXaGNOTXpVd05ERTVNVE0wTVRBNApXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTkRVeU5ESTROamd3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFUR2U2SW5hQko4RGRiQVdmZTRNM09yS3ZTR1IvWkROS0JJV2lMejZXckcKYmh4Mm5ncHJFQjhRYThEWDZocGRCVkI1RmN3L1hwTXZjRnNCeFh3MThPNkdvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVWRXOEFySENLNVQ2OFdJeG1BUnlTCkJFcHJ6N1F3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnYlhwYXV0c2RlTlJHNm4zRmhBRkkzbncvWUErYTBBdi8KUE9HZzNhYWRCSDBDSUU5NW4vQW5yOXV2Z0tyczVrakpGKzAwci96SWpFdnQxNmhkOGdabFEzb1IKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + server: https://172.29.0.11:6443 + name: default +contexts: +- context: + cluster: default + user: default + name: default +current-context: default +kind: Config +preferences: {} +users: +- name: default + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJZm5OdzUyby9ybDB3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOelExTWpReU9EWTRNQjRYRFRJMU1EUXlNVEV6TkRFd09Gb1hEVEkyTURReQpNVEV6TkRFd09Gb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJHa3UxWDQvRGNMU1h4cXcKSzczdGhMUFd5Y3JCendrL3ZMbzkzMTZ0dXNXRWE4WnNMdytnYk5wWEVOSFNxQk8zdGcrYUFSdGhWakF3OU5GaApZWXNtaTQyalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCU1B5dEJrYlFaRmtxSEpOaHlHNjZ1K3ZqbjNhakFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlFQXZQVC9ENGs2S29tUUxaZjRNMTEvd1psT3QyZkpGTGhIY3EyOHMvUUVLcndDSUZVMWVzTGFFTnRWc2xUeAo0QTFtNkt4eEpUbmdNM0M3UXJ5OUs0Z2hzVDA0Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTkRVeU5ESTROamd3SGhjTk1qVXdOREl4TVRNME1UQTRXaGNOTXpVd05ERTVNVE0wTVRBNApXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTkRVeU5ESTROamd3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFSeENGL2FuNVUvS0xEUk9VUlZxdllWTHFsdWxXaE8rMzJpc1lRU1hlYUgKdnRBWHo3dFE2MkhrazNIdXZudnFmditvQlBKVUlzS3ExTyt6aytLT3JWa2dvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVWo4clFaRzBHUlpLaHlUWWNodXVyCnZyNDU5Mm93Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnWmxiRkpoZWwzQiszQ2FyZjUrdE5taDhkWkRUOC9yVDAKR01VVXlBNzlWSUVDSVFDSUQ1NkVQbmtOa0xmZExEZXloclBGWks3VWRTc2x2QWZGN2N4czF1MDBxZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUpMOGJrS1FMbEdZWUE0Ny9Lek9CRm5iZnI0NGQ5VndUWXlPT0dJdnNLOXNvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFYVM3VmZqOE53dEpmR3JBcnZlMkVzOWJKeXNIUENUKzh1ajNmWHEyNnhZUnJ4bXd2RDZCcwoybGNRMGRLb0U3ZTJENW9CRzJGV01ERDAwV0ZoaXlhTGpRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo= diff --git a/e2e/k8s/data/config/node-token.txt b/e2e/k8s/data/config/node-token.txt new file mode 100644 index 0000000..190bd76 --- /dev/null +++ b/e2e/k8s/data/config/node-token.txt @@ -0,0 +1 @@ +K10800b7e6b1f6390067e433bd30dff5f925893b60074f8c36f7725bfdce6a45917::server:ca7449eb0dddd8a4bb6e5ca86e2d0cdd diff --git a/e2e/k8s/data/create_k8s_monitor_data.json b/e2e/k8s/data/create_k8s_monitor_data.json new file mode 100644 index 0000000..c7f0648 --- /dev/null +++ b/e2e/k8s/data/create_k8s_monitor_data.json @@ -0,0 +1,37 @@ +{ + "detected": true, + "monitor": { + "intervals": 10, + "tags": [], + "app": "kubernetes", + "instance": "172.29.0.11", + "name": "Brave_Stingray_55yR" + }, + "collector": "", + "params": [ + { + "display": true, + "field": "host", + "type": 1, + "paramValue": "172.29.0.11" + }, + { + "display": true, + "field": "port", + "type": 0, + "paramValue": 6443 + }, + { + "display": true, + "field": "authType", + "type": 1, + "paramValue": "Bearer Token" + }, + { + "display": true, + "field": "token", + "type": 1, + "paramValue": "#TOKEN#" + } + ] +} diff --git a/e2e/k8s/data/testsuite.yaml b/e2e/k8s/data/testsuite.yaml new file mode 100644 index 0000000..346f4e5 --- /dev/null +++ b/e2e/k8s/data/testsuite.yaml @@ -0,0 +1,69 @@ +#!api-testing +# 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. + +# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-schema.json +name: hertzbeat +api: | + {{default "http://localhost:1157" (env "SERVER")}} +param: + monitorHTTP: "{{randAlpha 6}}" + monitorSiteMap: "{{randAlpha 6}}" + monitorPort: "{{randAlpha 6}}" + monitorSSL: "{{randAlpha 6}}" + monitorPing: "{{randAlpha 6}}" + monitorUDP: "{{randAlpha 6}}" + monitorWebsite: "{{randAlpha 6}}" + monitorFTP: "{{randAlpha 6}}" + tagName: "{{randAlpha 3}}" +items: +- name: login + request: + api: /api/account/auth/form + method: POST + header: + Content-type: application/json + body: | + { + "type": 0, + "identifier": "admin", + "credential": "hertzbeat" + } + expect: + bodyFieldsExpect: + code: "0" +- name: detectMonitor + request: + api: /api/monitor/detect + method: POST + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json + bodyFromFile: data/tmp.json + expect: + statusCode: 200 +- name: createMonitor + request: + api: /api/monitor + method: POST + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json + bodyFromFile: data/tmp.json + expect: + statusCode: 200 + diff --git a/e2e/k8s/docker-compose.yml b/e2e/k8s/docker-compose.yml new file mode 100644 index 0000000..9304f7e --- /dev/null +++ b/e2e/k8s/docker-compose.yml @@ -0,0 +1,123 @@ +# 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. + +version: '3.8' +services: + init: + image: bitnami/kubectl:latest + privileged: true + depends_on: + k3s: + condition: service_healthy + entrypoint: /bin/bash + networks: + k3s_network: + ipv4_address: 172.29.0.10 + volumes: + - ./data/config/k3s.yaml:/.kube/config + - ./data:/testcase + command: + - -c + - | + # Wait for k3s to start + until kubectl cluster-info; do sleep 1; done + sleep 5 + # Create serviceaccount and clusterrolebinding + kubectl create serviceaccount -n kube-system cluster-admin-sa + kubectl create clusterrolebinding -n kube-system cluster-admin-binding --clusterrole=cluster-admin --serviceaccount=kube-system:cluster-admin-sa + cd /testcase + kubectl create token cluster-admin-sa -n kube-system --duration=24h + echo $(sed "s/#TOKEN#/$(echo $(kubectl create token cluster-admin-sa -n kube-system --duration=24h))/g" create_k8s_monitor_data.json)>tmp.json + # install metrics server + kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml + sleep 30 + tail -f /dev/null + k3s: + image: rancher/k3s:v1.31.0-k3s1 + container_name: k3s + privileged: true + ports: + - "6443:6443" + - "80:80" + command: server --bind-address 172.29.0.11 + networks: + k3s_network: + ipv4_address: 172.29.0.11 + healthcheck: + test: [ "CMD-SHELL", + "kubectl cluster-info && echo $(cat /var/lib/rancher/k3s/server/node-token)>node-token.txt" ] + interval: 3s + retries: 30 + start_period: 10s + timeout: 10s + volumes: + - ./data/config/k3s.yaml:/etc/rancher/k3s/k3s.yaml + - ./data/config/node-token.txt:/node-token.txt + testing: + image: ghcr.io/linuxsuren/api-testing:v0.0.17 + environment: + SERVER: http://hertzbeat:1157 + container_name: e2e-testing + volumes: + - ./data/:/work/data/ + - ./data/testsuite.yaml:/work/testsuite.yaml + command: atest run -p /work/testsuite.yaml --level debug --thread 3 + networks: + k3s_network: + ipv4_address: 172.29.0.12 + depends_on: + checker: + condition: service_healthy + k3s: + condition: service_healthy + links: + - hertzbeat + - checker + - k3s + checker: + image: alpine:latest + container_name: e2e-checker + command: sh -c "apk add --update curl && tail -f /dev/null" + healthcheck: + test: [ "CMD-SHELL", + "curl -s -X POST -H \"Content-Type: application/json\" -d '{\"type\": 0,\"identifier\": \"admin\",\"credential\": \"hertzbeat\"}' http://hertzbeat:1157/api/account/auth/form | grep -q token" + ] + interval: 3s + timeout: 60s + retries: 10 + start_period: 10s + networks: + k3s_network: + ipv4_address: 172.29.0.13 + depends_on: + - init + hertzbeat: + image: apache/hertzbeat:test + container_name: e2e-hertzbeat + volumes: + - ./logs/:/opt/hertzbeat/logs/ + ports: + - "1157:1157" + networks: + k3s_network: + ipv4_address: 172.29.0.14 +networks: + k3s_network: + driver: bridge + ipam: + config: + - subnet: 172.29.0.0/16 diff --git a/e2e/report/report.md b/e2e/report/report.md new file mode 100644 index 0000000..43ed152 --- /dev/null +++ b/e2e/report/report.md @@ -0,0 +1 @@ +keep, not delete! diff --git a/e2e/testsuite.yaml b/e2e/testsuite.yaml new file mode 100644 index 0000000..1fa1711 --- /dev/null +++ b/e2e/testsuite.yaml @@ -0,0 +1,478 @@ +#!api-testing +# 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. + +# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-schema.json +name: hertzbeat +api: | + {{default "http://localhost:1157" (env "SERVER")}} +param: + monitorHTTP: "{{randAlpha 6}}" + monitorSiteMap: "{{randAlpha 6}}" + monitorPort: "{{randAlpha 6}}" + monitorSSL: "{{randAlpha 6}}" + monitorPing: "{{randAlpha 6}}" + monitorUDP: "{{randAlpha 6}}" + monitorWebsite: "{{randAlpha 6}}" + monitorFTP: "{{randAlpha 6}}" + apiTokenName: "e2e-token-{{randAlpha 6}}" + labelName: "{{randAlpha 3}}" +items: +- name: login + request: + api: /api/account/auth/form + method: POST + header: + Content-type: application/json + body: | + { + "type": 0, + "identifier": "admin", + "credential": "hertzbeat" + } + expect: + bodyFieldsExpect: + code: "0" +- name: login-missing-content-type + request: + api: /api/account/auth/form + method: POST + body: | + { + "type": 0, + "identifier": "admin", + "credential": "hertzbeat" + } + expect: + statusCode: 500 +- name: monitorList + request: + api: /api/monitors?pageIndex=0&pageSize=8 + header: + Authorization: Bearer {{.login.data.token}} + expect: + bodyFieldsExpect: + code: "0" +- name: missing-auth-header + request: + api: /api/monitors + header: + Accept: application/json + expect: + statusCode: 401 +- name: createSitemapMonitor + request: + api: /api/monitor + method: POST + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json + bodyFromFile: data/monitor-sitemap.json +- name: listSitemapMonitor + request: + api: /api/monitors?pageIndex=0&pageSize=8&app=fullsite + header: + Authorization: Bearer {{.login.data.token}} +- name: cancelSitemapMonitor + request: + api: /api/monitors/manage?ids={{(index .listSitemapMonitor.data.content 0).id | int64}} + query: + type: JSON + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} +- name: deleteSitemapMonitor + request: + api: /api/monitors?ids={{(index .listSitemapMonitor.data.content 0).id | int64}} + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} + +- name: createPortMonitor + request: + api: /api/monitor + method: POST + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json + bodyFromFile: data/monitor-port.json +- name: listPortMonitor + request: + api: /api/monitors?pageIndex=0&pageSize=8&app=port + header: + Authorization: Bearer {{.login.data.token}} +- name: cancelPortMonitor + request: + api: /api/monitors/manage?ids={{(index .listPortMonitor.data.content 0).id | int64}} + query: + type: JSON + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} +- name: deletePortMonitor + request: + api: /api/monitors?ids={{(index .listPortMonitor.data.content 0).id | int64}} + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} + +- name: createSSLMonitor + request: + api: /api/monitor + method: POST + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json + bodyFromFile: data/monitor-ssl.json +- name: listSSLMonitor + request: + api: /api/monitors?pageIndex=0&pageSize=8&app=ssl_cert + header: + Authorization: Bearer {{.login.data.token}} +- name: cancelSSLMonitor + request: + api: /api/monitors/manage?ids={{(index .listSSLMonitor.data.content 0).id | int64}} + query: + type: JSON + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} +- name: deleteSSLMonitor + request: + api: /api/monitors?ids={{(index .listSSLMonitor.data.content 0).id | int64}} + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} + +- name: createPingMonitor + request: + api: /api/monitor + method: POST + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json + bodyFromFile: data/monitor-ping.json +- name: listPingMonitor + request: + api: /api/monitors?pageIndex=0&pageSize=8&app=ping + header: + Authorization: Bearer {{.login.data.token}} +- name: cancelPingMonitor + request: + api: /api/monitors/manage?ids={{(index .listPingMonitor.data.content 0).id | int64}} + query: + type: JSON + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} +- name: deletePingMonitor + request: + api: /api/monitors?ids={{(index .listPingMonitor.data.content 0).id | int64}} + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} + +- name: createUDPMonitor + request: + api: /api/monitor + method: POST + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json + bodyFromFile: data/monitor-udp.json +- name: listUDPMonitor + request: + api: /api/monitors?pageIndex=0&pageSize=8&app=udp_port + header: + Authorization: Bearer {{.login.data.token}} +- name: cancelUDPMonitor + request: + api: /api/monitors/manage?ids={{(index .listUDPMonitor.data.content 0).id | int64}} + query: + type: JSON + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} +- name: deleteUDPMonitor + request: + api: /api/monitors?ids={{(index .listUDPMonitor.data.content 0).id | int64}} + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} + +- name: createWebsiteMonitor + request: + api: /api/monitor + method: POST + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json + bodyFromFile: data/monitor-website.json +- name: listWebsiteMonitor + request: + api: /api/monitors?pageIndex=0&pageSize=8&app=website + header: + Authorization: Bearer {{.login.data.token}} +- name: cancelWebsiteMonitor + request: + api: /api/monitors/manage?ids={{(index .listWebsiteMonitor.data.content 0).id | int64}} + query: + type: JSON + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} +- name: deleteWebsiteMonitor + request: + api: /api/monitors?ids={{(index .listWebsiteMonitor.data.content 0).id | int64}} + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} + +- name: createFTPMonitor + request: + api: /api/monitor + method: POST + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json + bodyFromFile: data/monitor-ftp.json +- name: listFTPMonitor + request: + api: /api/monitors?pageIndex=0&pageSize=8&app=ftp + header: + Authorization: Bearer {{.login.data.token}} +- name: cancelFTPMonitor + request: + api: /api/monitors/manage?ids={{(index .listFTPMonitor.data.content 0).id | int64}} + query: + type: JSON + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} +- name: deleteFTPMonitor + request: + api: /api/monitors?ids={{(index .listFTPMonitor.data.content 0).id | int64}} + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} + +- name: createHTTPMonitor + request: + api: /api/monitor + method: POST + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json + bodyFromFile: data/monitor-http.json +- name: listHTTPMonitor + request: + api: /api/monitors?pageIndex=0&pageSize=8&app=api + header: + Authorization: Bearer {{.login.data.token}} +- name: cancelHTTPMonitor + request: + api: /api/monitors/manage?ids={{(index .listHTTPMonitor.data.content 0).id | int64}} + query: + type: JSON + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} +- name: deleteHTTPMonitor + request: + api: /api/monitors?ids={{(index .listHTTPMonitor.data.content 0).id | int64}} + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} + +- name: listAlertDefines + request: + api: /api/alert/defines + header: + Authorization: Bearer {{.login.data.token}} +- name: deleteAlertDefine + request: + api: /api/alert/defines?ids=1 + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} +- name: listAlertGroupConverges + request: + api: /api/alert/groups + header: + Authorization: Bearer {{.login.data.token}} +- name: listAlertSilences + request: + api: /api/alert/silences + header: + Authorization: Bearer {{.login.data.token}} +- name: listGroupAlerts + request: + api: /api/alerts/group + header: + Authorization: Bearer {{.login.data.token}} +- name: listReceivers + request: + api: /api/notice/receivers + header: + Authorization: Bearer {{.login.data.token}} +- name: listRules + request: + api: /api/notice/rules + header: + Authorization: Bearer {{.login.data.token}} +- name: listTemplates + request: + api: /api/notice/templates + header: + Authorization: Bearer {{.login.data.token}} +- name: listCollectors + request: + api: /api/collector + header: + Authorization: Bearer {{.login.data.token}} + +## Label +- name: createLabel + request: + api: /api/label + method: POST + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json + body: | + { + "name": "{{.param.labelName}}" + } +- name: listLabels + request: + api: /api/label?pageIndex=0&pageSize=8&type=1 + header: + Authorization: Bearer {{.login.data.token}} +- name: updateLabel + request: + api: /api/label + method: PUT + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json + body: | + { + "id": {{(index .listLabels.data.content 0).id | int64}}, + "name": "{{randAlpha 3}}", + "tagValue": "{{randAlpha 3}}", + "type": 1, + "creator": "admin", + "modifier": "admin" + } + expect: + bodyFieldsExpect: + code: "0" +- name: deleteLabel + request: + api: /api/label?ids={{(index .listLabels.data.content 0).id | int64}} + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} + +## API Token Management +- name: generateApiToken + request: + api: /api/account/token/generate?name={{.apiTokenName}}&expireSeconds=-1 + method: POST + header: + Authorization: Bearer {{.login.data.token}} + expect: + bodyFieldsExpect: + code: "0" +- name: listApiTokens + request: + api: /api/account/token + header: + Authorization: Bearer {{.login.data.token}} + expect: + bodyFieldsExpect: + code: "0" + data.0.name: "{{.apiTokenName}}" + data.0.creator: "admin" +- name: listApiTokensByGeneratedToken + request: + api: /api/account/token + header: + Authorization: Bearer {{.generateApiToken.data.token}} + expect: + bodyFieldsExpect: + code: "0" + data.0.name: "{{.apiTokenName}}" +- name: useGeneratedTokenToCallApi + request: + api: /api/monitors?pageIndex=0&pageSize=8 + header: + Authorization: Bearer {{.generateApiToken.data.token}} + expect: + bodyFieldsExpect: + code: "0" +- name: deleteApiToken + request: + api: /api/account/token/{{(index .listApiTokens.data 0).id | int64}} + method: DELETE + header: + Authorization: Bearer {{.login.data.token}} + expect: + bodyFieldsExpect: + code: "0" + +## Config +- name: getConfigEmail + request: + api: /api/config/email + header: + Authorization: Bearer {{.login.data.token}} +- name: getConfigSystem + request: + api: /api/config/system + header: + Authorization: Bearer {{.login.data.token}} +- name: updateSystem + request: + api: /api/config/system + method: POST + body: | + { + "timeZoneId": "Asia/Shanghai", + "locale": "en_US", + "theme": null + } + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json +- name: getConfigOSS + request: + api: /api/config/oss + header: + Authorization: Bearer {{.login.data.token}} +- name: updateOSS + request: + api: /api/config/oss + method: POST + body: | + { + "type": "FILE", + "config": {} + } + header: + Authorization: Bearer {{.login.data.token}} + Content-type: application/json diff --git a/hertzbeat-ai/pom.xml b/hertzbeat-ai/pom.xml new file mode 100644 index 0000000..249c6be --- /dev/null +++ b/hertzbeat-ai/pom.xml @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + <artifactId>hertzbeat-ai</artifactId> + <version>${hertzbeat.version}</version> + <properties> + <spring-ai.version>2.0.0</spring-ai.version> + <java.version>25</java.version> + </properties> + + <dependencies> + <dependency> + <groupId>org.springframework.ai</groupId> + <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-webflux</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.ai</groupId> + <artifactId>spring-ai-openai</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.ai</groupId> + <artifactId>spring-ai-client-chat</artifactId> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-common-spring</artifactId> + </dependency> + <!-- common --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-base</artifactId> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-alerter</artifactId> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-manager</artifactId> + </dependency> + <dependency> + <groupId>com.usthe.sureness</groupId> + <artifactId>spring-boot3-starter-sureness</artifactId> + </dependency> + + </dependencies> + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.ai</groupId> + <artifactId>spring-ai-bom</artifactId> + <version>${spring-ai.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <configuration> + <skip>true</skip> + </configuration> + </plugin> + </plugins> + </build> + +</project> diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/LlmConfig.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/LlmConfig.java new file mode 100644 index 0000000..bd56377 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/LlmConfig.java @@ -0,0 +1,151 @@ +/* + * 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 lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent; +import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; + +/** + * Configuration class for Large Language Model (LLM) settings. + */ + +@Configuration +@Slf4j +public class LlmConfig { + + private final GeneralConfigDao generalConfigDao; + + private ApplicationContext applicationContext; + + public LlmConfig(GeneralConfigDao generalConfigDao, ApplicationContext applicationContext) { + this.generalConfigDao = generalConfigDao; + this.applicationContext = applicationContext; + } + + /** + * Create ChatClient bean with all dependencies created internally + */ + @Bean + public ChatClient openAiChatClient() { + return createChatClient(); + } + + /** + * Create ChatClient with all necessary components + */ + private ChatClient createChatClient() { + + GeneralConfig providerConfig = generalConfigDao.findByType("provider"); + if (providerConfig == null || providerConfig.getContent() == null) { + log.warn("LLM Provider is not set, ChatClient bean will not be created"); + return null; + } + ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class); + + if (modelProviderConfig == null || modelProviderConfig.getApiKey() == null) { + log.warn("LLM Provider configuration is incomplete, ChatClient bean will not be created"); + return null; + } + + if (modelProviderConfig.getBaseUrl() == null) { + if ("openai".equals(modelProviderConfig.getCode())) { + modelProviderConfig.setBaseUrl("https://api.openai.com/v1"); + } else if ("zhipu".equals(modelProviderConfig.getCode())) { + modelProviderConfig.setBaseUrl("https://open.bigmodel.cn/api/paas/v4"); + } else if ("zai".equals(modelProviderConfig.getCode())) { + modelProviderConfig.setBaseUrl("https://api.z.ai/api/paas/v4"); + } else { + modelProviderConfig.setBaseUrl("https://api.openai.com/v1"); + } + } + + if (modelProviderConfig.getModel() == null) { + if ("openai".equals(modelProviderConfig.getCode())) { + modelProviderConfig.setModel("gpt-5"); + } else if ("zhipu".equals(modelProviderConfig.getCode())) { + modelProviderConfig.setModel("glm-4.6"); + } else if ("zai".equals(modelProviderConfig.getCode())) { + modelProviderConfig.setModel("glm-4.6"); + } else { + modelProviderConfig.setModel("gpt-5"); + } + } + + // Create Chat Options with baseUrl and apiKey + OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder() + .baseUrl(modelProviderConfig.getBaseUrl()) + .apiKey(modelProviderConfig.getApiKey()) + .model(modelProviderConfig.getModel()) + .temperature(0.3) + .build(); + + // Create Chat Model + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .options(openAiChatOptions) + .build(); + + // Create and return ChatClient + return ChatClient.create(openAiChatModel); + } + + /** + * AI configuration change event listener + * Uses ApplicationContext to unregister and re-register the ChatClient bean + */ + @EventListener(AiProviderConfigChangeEvent.class) + public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) { + log.info("Provider configuration change event received, refreshing ChatClient bean"); + + try { + ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext; + DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableContext.getBeanFactory(); + + // Remove the existing ChatClient bean + if (beanFactory.containsSingleton("openAiChatClient")) { + beanFactory.destroySingleton("openAiChatClient"); + log.info("Existing ChatClient bean destroyed"); + } + + // Create new ChatClient with updated configuration + ChatClient newChatClient = createChatClient(); + + // Register the new ChatClient bean + beanFactory.registerSingleton("openAiChatClient", newChatClient); + + log.info("ChatClient bean refreshed successfully with new AI provider configuration"); + + } catch (Exception e) { + log.error("Failed to refresh ChatClient bean after configuration change", e); + } + } + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/McpContextHolder.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/McpContextHolder.java new file mode 100644 index 0000000..a41b8f3 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/McpContextHolder.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.ai.config; + +import com.usthe.sureness.subject.SubjectSum; +import org.springframework.core.NamedInheritableThreadLocal; + +/** + * Context holder for AI agent security context. + */ +public final class McpContextHolder { + private static final ThreadLocal<SubjectSum> subjectHolder = + new NamedInheritableThreadLocal<>("MCP Security and User Identification Context"); + + private McpContextHolder() {} + + /** + * Attaches the user's context to the current thread. + */ + public static void setSubject(SubjectSum subject) { + + subjectHolder.set(subject); + } + + /** + * Retrieves the context from the current thread. + */ + public static SubjectSum getSubject() { + return subjectHolder.get(); + } + + /** + * Clears the context from the thread to prevent memory leaks. + */ + public static void clear() { + subjectHolder.remove(); + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/SopI18nConfig.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/SopI18nConfig.java new file mode 100644 index 0000000..dc75aea --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/SopI18nConfig.java @@ -0,0 +1,39 @@ +/* + * 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.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ResourceBundleMessageSource; + +/** + * Configuration for SOP internationalization. + */ +@Configuration +public class SopI18nConfig { + + @Bean("sopMessageSource") + public MessageSource sopMessageSource() { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename("i18n/messages"); + messageSource.setDefaultEncoding("UTF-8"); + messageSource.setUseCodeAsDefaultMessage(true); + return messageSource; + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/ChatController.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/ChatController.java new file mode 100644 index 0000000..0f5bfcd --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/ChatController.java @@ -0,0 +1,169 @@ +/* + * 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.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; +import org.apache.hertzbeat.ai.service.ConversationService; +import org.apache.hertzbeat.common.entity.ai.ChatConversation; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.http.codec.ServerSentEvent; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +import jakarta.validation.Valid; +import java.util.List; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE; + +/** + * Controller class for handling AI chat requests and conversation management. + */ +@Slf4j +@Tag(name = "AI Chat API") +@RestController +@RequestMapping(path = "/api/chat", produces = {APPLICATION_JSON_VALUE}) +public class ChatController { + + private final ConversationService conversationService; + + @Autowired + public ChatController(ConversationService conversationService) { + this.conversationService = conversationService; + } + + /** + * Send a message and get a streaming response with conversation tracking + * + * @param context The chat request context containing message and optional conversationId + * @return Flux of ServerSentEvent for streaming response + */ + @PostMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE) + @Operation(summary = "Send a chat message with streaming response", description = "Send a message to AI and get a streaming response with conversation tracking") + 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()) + .response("Error: Message cannot be empty") + .build(); + return Flux.just(ServerSentEvent.builder(errorResponse) + .event("error") + .build()); + } + + log.info("Received streaming chat request for conversation: {}", context.getConversationId()); + return conversationService.streamChat(context.getMessage(), context.getConversationId()); + + } catch (Exception e) { + log.error("Error in stream chat endpoint: ", e); + ChatResponseChunk errorResponse = ChatResponseChunk.builder() + .conversationId(context.getConversationId()) + .response("An error occurred: " + e.getMessage()) + .build(); + return Flux.just(ServerSentEvent.builder(errorResponse) + .event("error") + .build()); + } + } + + /** + * Create a new conversation + * + * @return Created conversation details + */ + @PostMapping(path = "/conversations") + @Operation(summary = "Create a new conversation", description = "Create a new conversation") + public ResponseEntity<Message<ChatConversation>> createConversation() { + ChatConversation conversation = conversationService.createConversation(); + return ResponseEntity.ok(Message.success(conversation)); + } + + /** + * Get all conversations + * + * @return List of all conversations + */ + @GetMapping(path = "/conversations") + @Operation(summary = "List all conversations", description = "Get a list of all conversations") + public ResponseEntity<Message<List<ChatConversation>>> listConversations() { + List<ChatConversation> conversations = conversationService.getAllConversations(); + return ResponseEntity.ok(Message.success(conversations)); + } + + /** + * Get conversation history + * + * @param conversationId The conversation ID + * @return Conversation details with message history + */ + @GetMapping(path = "/conversations/{conversationId}") + @Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation") + public ResponseEntity<Message<ChatConversation>> getConversation( + @Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) { + ChatConversation conversation = conversationService.getConversation(conversationId); + return ResponseEntity.ok(Message.success(conversation)); + } + + /** + * Delete a conversation + * + * @param conversationId The conversation ID to delete + * @return Success or error message + */ + @DeleteMapping(path = "/conversations/{conversationId}") + @Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages") + public ResponseEntity<Message<Void>> deleteConversation( + @Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) { + conversationService.deleteConversation(conversationId); + return ResponseEntity.ok(Message.success()); + } + + /** + * Save data submitted by secure form + * @param securityData security data + * @return save result + */ + @PostMapping(path = "/security") + @Operation(summary = "save security data", description = "Save security data") + public ResponseEntity<Message<Boolean>> commitSecurityData(@Valid @RequestBody SecurityData securityData) { + return ResponseEntity.ok(Message.success(conversationService.saveSecurityData(securityData))); + } + + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/SopController.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/SopController.java new file mode 100644 index 0000000..d2b0f31 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/SopController.java @@ -0,0 +1,180 @@ +/* + * 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.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.sop.engine.SopEngine; +import org.apache.hertzbeat.ai.sop.model.SopDefinition; +import org.apache.hertzbeat.ai.sop.model.SopResult; +import org.apache.hertzbeat.ai.sop.registry.SkillRegistry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +/** + * Controller for AI SOP (Standard Operating Procedure) execution. + */ +@Slf4j +@RestController +@RequestMapping("/api/ai/sop") +@Tag(name = "AI SOP", description = "AI Standard Operating Procedure Management") +public class SopController { + + private final SkillRegistry skillRegistry; + private final SopEngine sopEngine; + + @Autowired + public SopController(SkillRegistry skillRegistry, SopEngine sopEngine) { + this.skillRegistry = skillRegistry; + this.sopEngine = sopEngine; + } + + /** + * List all available SOP skills. + */ + @GetMapping("/skills") + @Operation(summary = "List available SOP skills", + description = "Get all registered SOP skill definitions") + public ResponseEntity<List<Map<String, String>>> listSkills() { + List<Map<String, String>> skills = skillRegistry.getAllSkills().stream() + .map(skill -> { + Map<String, String> info = new HashMap<>(); + info.put("name", skill.getName()); + info.put("description", skill.getDescription()); + info.put("version", skill.getVersion()); + return info; + }) + .collect(Collectors.toList()); + return ResponseEntity.ok(skills); + } + + /** + * Get details of a specific SOP skill. + */ + @GetMapping("/skills/{skillName}") + @Operation(summary = "Get SOP skill details", + description = "Get detailed definition of a specific SOP skill") + public ResponseEntity<SopDefinition> getSkillDetails( + @Parameter(description = "Name of the SOP skill") + @PathVariable String skillName) { + SopDefinition skill = skillRegistry.getSkill(skillName); + if (skill == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(skill); + } + + /** + * Execute a SOP skill with streaming output (SSE mode). + * Use this for real-time progress updates in UI. + */ + @PostMapping(value = "/execute/{skillName}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @Operation(summary = "Execute SOP skill (streaming)", + description = "Execute a SOP skill with streaming output for real-time progress") + public Flux<String> executeSopStream( + @Parameter(description = "Name of the SOP skill to execute") + @PathVariable String skillName, + @Parameter(description = "Input parameters for the SOP") + @RequestBody(required = false) Map<String, Object> params) { + + log.info("Executing SOP skill (stream): {} with params: {}", skillName, params); + + SopDefinition skill = skillRegistry.getSkill(skillName); + if (skill == null) { + return Flux.just("Error: SOP skill not found: " + skillName); + } + + Map<String, Object> inputParams = params != null ? params : new HashMap<>(); + + return sopEngine.execute(skill, inputParams) + .doOnNext(msg -> log.debug("SOP output: {}", msg)) + .doOnError(e -> log.error("SOP execution error: {}", e.getMessage())) + .doOnComplete(() -> log.info("SOP {} execution completed", skillName)); + } + + /** + * Execute a SOP skill synchronously and return unified result. + * Use this for AI tool calls and programmatic access. + */ + @PostMapping(value = "/execute/{skillName}/sync", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Execute SOP skill (sync)", + description = "Execute a SOP skill synchronously and return unified result") + public ResponseEntity<SopResult> executeSopSync( + @Parameter(description = "Name of the SOP skill to execute") + @PathVariable String skillName, + @Parameter(description = "Input parameters for the SOP") + @RequestBody(required = false) Map<String, Object> params) { + + log.info("Executing SOP skill (sync): {} with params: {}", skillName, params); + + SopDefinition skill = skillRegistry.getSkill(skillName); + if (skill == null) { + SopResult errorResult = SopResult.builder() + .sopName(skillName) + .status("FAILED") + .error("SOP skill not found: " + skillName) + .build(); + return ResponseEntity.notFound().build(); + } + + Map<String, Object> inputParams = params != null ? params : new HashMap<>(); + + SopResult result = sopEngine.executeSync(skill, inputParams); + return ResponseEntity.ok(result); + } + + /** + * Execute a SOP skill and return AI-friendly text response. + * Use this when SOP is called as a tool by AI. + */ + @PostMapping(value = "/execute/{skillName}/ai", produces = MediaType.TEXT_PLAIN_VALUE) + @Operation(summary = "Execute SOP skill (AI format)", + description = "Execute a SOP and return AI-friendly text response") + public ResponseEntity<String> executeSopForAi( + @Parameter(description = "Name of the SOP skill to execute") + @PathVariable String skillName, + @Parameter(description = "Input parameters for the SOP") + @RequestBody(required = false) Map<String, Object> params) { + + log.info("Executing SOP skill (AI): {} with params: {}", skillName, params); + + SopDefinition skill = skillRegistry.getSkill(skillName); + if (skill == null) { + return ResponseEntity.ok("Error: SOP skill not found: " + skillName); + } + + Map<String, Object> inputParams = params != null ? params : new HashMap<>(); + + SopResult result = sopEngine.executeSync(skill, inputParams); + return ResponseEntity.ok(result.toAiResponse()); + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/SopScheduleController.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/SopScheduleController.java new file mode 100644 index 0000000..167f438 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/SopScheduleController.java @@ -0,0 +1,131 @@ +/* + * 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.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.apache.hertzbeat.ai.service.SopScheduleService; +import org.apache.hertzbeat.common.entity.ai.SopSchedule; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST Controller for managing scheduled SOP executions. + */ +@Tag(name = "SOP Schedule API", description = "Manage scheduled SOP executions") +@RestController +@RequestMapping("/api/ai/schedule") +public class SopScheduleController { + + private final SopScheduleService sopScheduleService; + private final org.apache.hertzbeat.ai.sop.registry.SkillRegistry skillRegistry; + + @Autowired + public SopScheduleController(SopScheduleService sopScheduleService, + org.apache.hertzbeat.ai.sop.registry.SkillRegistry skillRegistry) { + this.sopScheduleService = sopScheduleService; + this.skillRegistry = skillRegistry; + } + + @Operation(summary = "Get all available SOP skills") + @GetMapping("/skills") + public ResponseEntity<Message<List<java.util.Map<String, String>>>> getAvailableSkills() { + List<java.util.Map<String, String>> skills = new java.util.ArrayList<>(); + for (var skill : skillRegistry.getAllSkills()) { + java.util.Map<String, String> skillInfo = new java.util.HashMap<>(); + skillInfo.put("name", skill.getName()); + skillInfo.put("description", skill.getDescription()); + skills.add(skillInfo); + } + return ResponseEntity.ok(Message.success(skills)); + } + + @Operation(summary = "Get all schedules for a conversation") + @GetMapping("/conversation/{conversationId}") + public ResponseEntity<Message<List<SopSchedule>>> getSchedulesByConversation( + @PathVariable Long conversationId) { + List<SopSchedule> schedules = sopScheduleService.getSchedulesByConversation(conversationId); + return ResponseEntity.ok(Message.success(schedules)); + } + + @Operation(summary = "Get a schedule by ID") + @GetMapping("/{id}") + public ResponseEntity<Message<SopSchedule>> getSchedule(@PathVariable Long id) { + SopSchedule schedule = sopScheduleService.getSchedule(id); + if (schedule == null) { + return ResponseEntity.ok(Message.fail((byte) 404, "Schedule not found")); + } + return ResponseEntity.ok(Message.success(schedule)); + } + + @Operation(summary = "Create a new scheduled SOP task") + @PostMapping + public ResponseEntity<Message<SopSchedule>> createSchedule(@RequestBody SopSchedule schedule) { + try { + SopSchedule created = sopScheduleService.createSchedule(schedule); + return ResponseEntity.ok(Message.success(created)); + } catch (IllegalArgumentException e) { + return ResponseEntity.ok(Message.fail((byte) 400, e.getMessage())); + } + } + + @Operation(summary = "Update an existing schedule") + @PutMapping("/{id}") + public ResponseEntity<Message<SopSchedule>> updateSchedule( + @PathVariable Long id, + @RequestBody SopSchedule schedule) { + try { + schedule.setId(id); + SopSchedule updated = sopScheduleService.updateSchedule(schedule); + return ResponseEntity.ok(Message.success(updated)); + } catch (IllegalArgumentException e) { + return ResponseEntity.ok(Message.fail((byte) 400, e.getMessage())); + } + } + + @Operation(summary = "Delete a schedule") + @DeleteMapping("/{id}") + public ResponseEntity<Message<Void>> deleteSchedule(@PathVariable Long id) { + sopScheduleService.deleteSchedule(id); + return ResponseEntity.ok(Message.success()); + } + + @Operation(summary = "Toggle schedule enabled status") + @PutMapping("/{id}/toggle") + public ResponseEntity<Message<SopSchedule>> toggleSchedule( + @PathVariable Long id, + @RequestParam boolean enabled) { + try { + SopSchedule updated = sopScheduleService.toggleSchedule(id, enabled); + return ResponseEntity.ok(Message.success(updated)); + } catch (IllegalArgumentException e) { + return ResponseEntity.ok(Message.fail((byte) 400, e.getMessage())); + } + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/ChatConversationDao.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/ChatConversationDao.java new file mode 100644 index 0000000..971b51f --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/ChatConversationDao.java @@ -0,0 +1,29 @@ +/* + * 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.dao; + +import org.apache.hertzbeat.common.entity.ai.ChatConversation; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * Repository interface for Conversation entities + */ +@Repository +public interface ChatConversationDao extends JpaRepository<ChatConversation, Long> { +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/ChatMessageDao.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/ChatMessageDao.java new file mode 100644 index 0000000..b0d0326 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/ChatMessageDao.java @@ -0,0 +1,45 @@ +/* + * 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.dao; + +import org.apache.hertzbeat.common.entity.ai.ChatMessage; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * Repository interface for Message entities + */ +@Repository +public interface ChatMessageDao extends JpaRepository<ChatMessage, Long> { + + /** + * Find all messages by conversation id, ordered by create time + * @param conversationId conversation id + * @return list of messages + */ + List<ChatMessage> findByConversationIdOrderByGmtCreateAsc(Long conversationId); + + /** + * Find all messages by conversation ids, ordered by create time + * @param conversationIds list of conversation ids + * @return list of messages + */ + List<ChatMessage> findByConversationIdInOrderByGmtCreateAsc(List<Long> conversationIds); +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/SopScheduleDao.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/SopScheduleDao.java new file mode 100644 index 0000000..0886159 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/SopScheduleDao.java @@ -0,0 +1,61 @@ +/* + * 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.dao; + +import java.time.LocalDateTime; +import java.util.List; +import org.apache.hertzbeat.common.entity.ai.SopSchedule; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +/** + * Repository for SopSchedule entity operations. + */ +@Repository +public interface SopScheduleDao extends JpaRepository<SopSchedule, Long>, JpaSpecificationExecutor<SopSchedule> { + + /** + * Find all schedules for a specific conversation. + * @param conversationId The conversation ID + * @return List of schedules + */ + List<SopSchedule> findByConversationId(Long conversationId); + + /** + * Find all enabled schedules that are due for execution. + * @param currentTime The current time to compare against + * @return List of due schedules + */ + @Query("SELECT s FROM SopSchedule s WHERE s.enabled = true AND s.nextRunTime <= :currentTime") + List<SopSchedule> findDueSchedules(@Param("currentTime") LocalDateTime currentTime); + + /** + * Find all enabled schedules. + * @return List of enabled schedules + */ + List<SopSchedule> findByEnabledTrue(); + + /** + * Delete all schedules for a conversation. + * @param conversationId The conversation ID + */ + void deleteByConversationId(Long conversationId); +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/ChatRequestContext.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/ChatRequestContext.java new file mode 100644 index 0000000..d0dcfac --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/ChatRequestContext.java @@ -0,0 +1,50 @@ +/* + * 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.pojo.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import org.apache.hertzbeat.common.entity.ai.ChatMessage; + +/** + * Chat request context for AI chat endpoint. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChatRequestContext { + /** + * The user's message (required) + */ + private String message; + /** + * Optional conversation ID for context + */ + private Long conversationId; + + /** + * Conversation history messages for context + */ + private List<ChatMessage> conversationHistory; +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/ChatResponseChunk.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/ChatResponseChunk.java new file mode 100644 index 0000000..0fd3ee6 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/ChatResponseChunk.java @@ -0,0 +1,48 @@ +/* + * 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.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Chat response DTO for AI responses. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Chat response") +public class ChatResponseChunk { + + @Schema(description = "Conversation ID", example = "123") + private Long conversationId; + + @Schema(description = "AI response message", example = "Here are your monitors...") + private String response; + + @Schema(description = "User message ID", example = "123") + private Long userMessageId; + + @Schema(description = "Assistant message ID", example = "123") + private Long assistantMessageId; + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/SecurityData.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/SecurityData.java new file mode 100644 index 0000000..fb01d1e --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/SecurityData.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.ai.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * security data + */ +@Data +public class SecurityData { + + @NotNull + @Schema(description = "Conversation ID", example = "123") + private Long conversationId; + + @NotNull + @Schema(description = "security data", example = "{\"password\":\"xxxxx\"}") + private String securityData; + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/schedule/SopScheduleExecutor.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/schedule/SopScheduleExecutor.java new file mode 100644 index 0000000..6e346f9 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/schedule/SopScheduleExecutor.java @@ -0,0 +1,175 @@ +/* + * 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.schedule; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.dao.ChatMessageDao; +import org.apache.hertzbeat.ai.service.SopScheduleService; +import org.apache.hertzbeat.ai.sop.engine.SopEngine; +import org.apache.hertzbeat.ai.sop.model.SopResult; +import org.apache.hertzbeat.ai.sop.registry.SkillRegistry; +import org.apache.hertzbeat.ai.utils.SopMessageUtil; +import org.apache.hertzbeat.common.entity.ai.ChatMessage; +import org.apache.hertzbeat.common.entity.ai.SopSchedule; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * Scheduled executor that checks for due SOP schedules and executes them. + * Runs every minute to check if any schedules are due for execution. + */ +@Slf4j +@Component +@EnableScheduling +public class SopScheduleExecutor { + + /** + * Role identifier for system push messages in conversations. + */ + public static final String ROLE_SYSTEM_PUSH = "system_push"; + + private final SopScheduleService sopScheduleService; + private final SopEngine sopEngine; + private final SkillRegistry skillRegistry; + private final ChatMessageDao chatMessageDao; + + @Autowired + public SopScheduleExecutor(SopScheduleService sopScheduleService, + @Lazy SopEngine sopEngine, + @Lazy SkillRegistry skillRegistry, + ChatMessageDao chatMessageDao) { + this.sopScheduleService = sopScheduleService; + this.sopEngine = sopEngine; + this.skillRegistry = skillRegistry; + this.chatMessageDao = chatMessageDao; + } + + /** + * Check for due schedules every minute and execute them. + */ + @Scheduled(fixedRate = 60000) // Every minute + public void checkAndExecuteDueSchedules() { + try { + List<SopSchedule> dueSchedules = sopScheduleService.getDueSchedules(); + + if (dueSchedules.isEmpty()) { + return; + } + + log.info("Found {} due schedules to execute", dueSchedules.size()); + + for (SopSchedule schedule : dueSchedules) { + executeSchedule(schedule); + } + } catch (Exception e) { + log.error("Error checking due schedules", e); + } + } + + /** + * Execute a single scheduled SOP and push result to conversation. + */ + private void executeSchedule(SopSchedule schedule) { + log.info("Executing scheduled SOP {} for conversation {}", + schedule.getSopName(), schedule.getConversationId()); + + try { + // Check if skill exists + var definition = skillRegistry.getSkill(schedule.getSopName()); + if (definition == null) { + log.warn("Skill {} not found, skipping schedule {}", + schedule.getSopName(), schedule.getId()); + return; + } + + // Parse parameters + Map<String, Object> params = new HashMap<>(); + if (schedule.getSopParams() != null && !schedule.getSopParams().isEmpty()) { + try { + params = JsonUtil.fromJson(schedule.getSopParams(), Map.class); + } catch (Exception e) { + log.warn("Failed to parse SOP params: {}", schedule.getSopParams()); + } + } + + // Execute SOP + SopResult result = sopEngine.executeSync(definition, params); + + // Create push message + String messageContent = formatPushMessage(schedule, result); + + // Save message to conversation + ChatMessage pushMessage = ChatMessage.builder() + .conversationId(schedule.getConversationId()) + .role(ROLE_SYSTEM_PUSH) + .content(messageContent) + .build(); + + chatMessageDao.save(pushMessage); + + log.info("Successfully pushed SOP result to conversation {}", + schedule.getConversationId()); + + } catch (Exception e) { + log.error("Failed to execute scheduled SOP {} for conversation {}", + schedule.getSopName(), schedule.getConversationId(), e); + + // Still save an error message + String errorContent = SopMessageUtil.getMessage("schedule.push.error.prefix") + " " + schedule.getSopName() + + "\n\n" + SopMessageUtil.getMessage("schedule.push.error.label") + " " + e.getMessage(); + ChatMessage errorMessage = ChatMessage.builder() + .conversationId(schedule.getConversationId()) + .role(ROLE_SYSTEM_PUSH) + .content(errorContent) + .build(); + chatMessageDao.save(errorMessage); + + } finally { + // Update schedule times + sopScheduleService.updateAfterExecution(schedule.getId()); + } + } + + /** + * Format the push message content with SOP result. + */ + private String formatPushMessage(SopSchedule schedule, SopResult result) { + StringBuilder sb = new StringBuilder(); + sb.append(SopMessageUtil.getMessage("schedule.push.report.title")).append("\n\n"); + sb.append(SopMessageUtil.getMessage("schedule.push.report.skill")).append(": ").append(schedule.getSopName()).append("\n"); + sb.append(SopMessageUtil.getMessage("schedule.push.report.time")).append(": ").append(LocalDateTime.now()).append("\n\n"); + sb.append("---\n\n"); + + if ("SUCCESS".equals(result.getStatus())) { + sb.append(result.getContent()); + } else { + sb.append(SopMessageUtil.getMessage("schedule.push.report.failed")).append("\n\n"); + sb.append(result.getError()); + } + + return sb.toString(); + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ChatClientProviderService.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ChatClientProviderService.java new file mode 100644 index 0000000..9946ba3 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ChatClientProviderService.java @@ -0,0 +1,42 @@ +/* + * 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; + +import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext; +import reactor.core.publisher.Flux; + +/** + * Service for interacting with LLM providers (like OpenAI, Anthropic, etc.) + */ +public interface ChatClientProviderService { + + /** + * Stream chat response from the LLM + * + * @param context Chat request context containing message and conversation history + * @return Flux of string chunks from the LLM response + */ + Flux<String> streamChat(ChatRequestContext context); + + /** + * Check if provider is properly configured + * @return true if configured and enabled + */ + boolean isConfigured(); +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ConversationService.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ConversationService.java new file mode 100644 index 0000000..8a007cf --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ConversationService.java @@ -0,0 +1,80 @@ +/* + * 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; + +import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk; +import org.apache.hertzbeat.ai.pojo.dto.SecurityData; +import org.apache.hertzbeat.common.entity.ai.ChatConversation; +import org.springframework.http.codec.ServerSentEvent; +import reactor.core.publisher.Flux; + +import java.util.List; + +/** + * Service for managing chat conversations and interactions with LLM providers. + */ +public interface ConversationService { + + /** + * Send a message and receive a streaming response + * + * @param message The user's message + * @param conversationId Optional conversation ID for continuing a chat + * @return Flux of ServerSentEvent for streaming the response + */ + Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId); + + /** + * Create a new conversation + * + * @return Created conversation data + */ + ChatConversation createConversation(); + + /** + * Get conversation history for a specific conversation + * + * @param conversationId Conversation ID + * @return Conversation data including messages + */ + ChatConversation getConversation(Long conversationId); + + /** + * Get all conversations for the current user + * + * @return List of conversations + */ + List<ChatConversation> getAllConversations(); + + /** + * Delete a conversation + * + * @param conversationId Conversation ID to delete + */ + void deleteConversation(Long conversationId); + + /** + * save security data for a conversation + * + * @param securityData securityData + * @return save result + */ + Boolean saveSecurityData(SecurityData securityData); + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/McpServerService.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/McpServerService.java new file mode 100644 index 0000000..695a26a --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/McpServerService.java @@ -0,0 +1,33 @@ +/* + * 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; + +import org.springframework.ai.tool.ToolCallbackProvider; + +/** + * Service interface for MCP server operations. + */ +public interface McpServerService { + + /** + * Provides the HertzBeat tools for the MCP server + * @return ToolCallbackProvider with all HertzBeat monitoring tools + */ + ToolCallbackProvider hertzbeatTools(); +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/SopScheduleService.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/SopScheduleService.java new file mode 100644 index 0000000..3f04938 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/SopScheduleService.java @@ -0,0 +1,81 @@ +/* + * 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; + +import java.util.List; +import org.apache.hertzbeat.common.entity.ai.SopSchedule; + +/** + * Service interface for managing scheduled SOP executions. + */ +public interface SopScheduleService { + + /** + * Create a new scheduled SOP task. + * @param schedule The schedule configuration + * @return The created schedule with generated ID + */ + SopSchedule createSchedule(SopSchedule schedule); + + /** + * Update an existing schedule. + * @param schedule The updated schedule + * @return The updated schedule + */ + SopSchedule updateSchedule(SopSchedule schedule); + + /** + * Delete a schedule by ID. + * @param id The schedule ID + */ + void deleteSchedule(Long id); + + /** + * Get a schedule by ID. + * @param id The schedule ID + * @return The schedule or null if not found + */ + SopSchedule getSchedule(Long id); + + /** + * Get all schedules for a conversation. + * @param conversationId The conversation ID + * @return List of schedules + */ + List<SopSchedule> getSchedulesByConversation(Long conversationId); + + /** + * Toggle the enabled status of a schedule. + * @param id The schedule ID + * @param enabled The new enabled status + * @return The updated schedule + */ + SopSchedule toggleSchedule(Long id, boolean enabled); + + /** + * Get all schedules that are due for execution. + * @return List of due schedules + */ + List<SopSchedule> getDueSchedules(); + + /** + * Update the execution times after a schedule runs. + * @param id The schedule ID + */ + void updateAfterExecution(Long id); +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java new file mode 100644 index 0000000..e744d3e --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java @@ -0,0 +1,220 @@ +/* + * 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 java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.sop.model.SopDefinition; +import org.apache.hertzbeat.ai.sop.model.SopParameter; +import org.apache.hertzbeat.ai.sop.registry.SkillRegistry; +import org.apache.hertzbeat.common.entity.ai.ChatMessage; +import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig; +import org.apache.hertzbeat.ai.service.ChatClientProviderService; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +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.springframework.ai.chat.prompt.SystemPromptTemplate; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.EventListener; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; +import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext; +import org.springframework.ai.chat.client.ChatClient; +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.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import reactor.core.publisher.Flux; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of the {@link ChatClientProviderService}. Provides functionality to interact with the ChatClient for + * handling chat messages. + */ +@Slf4j +@Service +public class ChatClientProviderServiceImpl implements ChatClientProviderService { + + private static final String SKILLS_PLACEHOLDER = "{dynamically_injected_skills_list}"; + private static final String CONVERSATION_ID_PLACEHOLDER = "{current_conversation_id}"; + + private final ApplicationContext applicationContext; + + private final GeneralConfigDao generalConfigDao; + + private ModelProviderConfig modelProviderConfig; + + + private final SkillRegistry skillRegistry; + + @Autowired + @Qualifier("hertzbeatTools") + private ToolCallbackProvider toolCallbackProvider; + + private boolean isConfigured = false; + + @Value("classpath:/prompt/system-message.st") + private Resource systemResource; + + @Value("classpath:/prompt/extra-message-protected.st") + private Resource extraResourceProtected; + + + @Autowired + public ChatClientProviderServiceImpl(ApplicationContext applicationContext, + GeneralConfigDao generalConfigDao, + @Lazy SkillRegistry skillRegistry) { + this.applicationContext = applicationContext; + this.generalConfigDao = generalConfigDao; + this.skillRegistry = skillRegistry; + } + + @Override + public Flux<String> streamChat(ChatRequestContext context) { + try { + // Get the current (potentially refreshed) ChatClient instance + ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class); + + List<Message> messages = new ArrayList<>(); + + // Add conversation history if available + if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) { + for (ChatMessage historyMessage : context.getConversationHistory()) { + if ("user".equals(historyMessage.getRole())) { + messages.add(new UserMessage(historyMessage.getContent())); + } else if ("assistant".equals(historyMessage.getRole())) { + messages.add(new AssistantMessage(historyMessage.getContent())); + } + } + } + + messages.add(new UserMessage(context.getMessage())); + + log.info("Starting streaming chat for conversation: {}", context.getConversationId()); + + // 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()); + + return chatClient.prompt() + .messages(messages) + .system(systemPrompt) + .toolCallbacks(toolCallbackProvider) + .stream() + .content() + .doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId())) + .doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error)); + + } catch (Exception e) { + log.error("Error setting up streaming chat: {}", e.getMessage(), e); + return Flux.error(e); + } + } + + /** + * Build the system prompt with dynamically injected skills list and conversation ID. + */ + private String buildSystemPrompt(Long conversationId) { + try { + String template = systemResource.getContentAsString(StandardCharsets.UTF_8); + String skillsList = generateSkillsList(); + template = template + .replace(SKILLS_PLACEHOLDER, skillsList) + .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")) { + Map<String, Object> metadata = new HashMap<>(); + metadata.put("conversationId", conversationId); + return template + SystemPromptTemplate.builder().resource(extraResourceProtected).build() + .create(metadata) + .getContents(); + } else { + return template; + } + + } catch (IOException e) { + log.error("Failed to read system prompt template: {}", e.getMessage()); + return ""; + } + } + + + /** + * Generate a formatted list of available skills for the system prompt. + */ + private String generateSkillsList() { + List<SopDefinition> skills = skillRegistry.getAllSkills(); + + if (skills.isEmpty()) { + return "No skills currently available. Use listSkills tool to refresh."; + } + + StringBuilder sb = new StringBuilder(); + for (SopDefinition skill : skills) { + sb.append("- **").append(skill.getName()).append("**: "); + sb.append(skill.getDescription()); + + // Add parameter hints + if (skill.getParameters() != null && !skill.getParameters().isEmpty()) { + sb.append(" (requires: "); + List<String> paramNames = new ArrayList<>(); + for (SopParameter param : skill.getParameters()) { + if (param.isRequired()) { + paramNames.add(param.getName()); + } + } + sb.append(String.join(", ", paramNames)); + sb.append(")"); + } + sb.append("\n"); + } + + return sb.toString(); + } + + @EventListener(AiProviderConfigChangeEvent.class) + public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) { + GeneralConfig providerConfig = generalConfigDao.findByType("provider"); + this.modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class); + } + + @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; + } + return isConfigured; + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImpl.java new file mode 100644 index 0000000..94c7605 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImpl.java @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.ai.service.impl; + +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.dao.ChatConversationDao; +import org.apache.hertzbeat.ai.dao.ChatMessageDao; +import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext; +import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk; +import org.apache.hertzbeat.ai.pojo.dto.SecurityData; +import org.apache.hertzbeat.ai.service.ChatClientProviderService; +import org.apache.hertzbeat.ai.service.ConversationService; +import org.apache.hertzbeat.common.entity.ai.ChatConversation; +import org.apache.hertzbeat.common.entity.ai.ChatMessage; +import org.apache.hertzbeat.common.util.AesUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.http.codec.ServerSentEvent; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import reactor.core.publisher.Flux; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * Implementation of the ConversationService interface for managing chat conversations. + */ +@Slf4j +@Service +public class ConversationServiceImpl implements ConversationService { + + @Autowired + private ChatConversationDao conversationDao; + + @Autowired + private ChatMessageDao messageDao; + + @Autowired + private ChatClientProviderService chatClientProviderService; + + @Override + public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId) { + + // Check if provider is properly configured + if (!chatClientProviderService.isConfigured()) { + ChatResponseChunk errorResponse = ChatResponseChunk.builder() + .conversationId(conversationId) + .response("Provider is not configured. Please configure your AI Provider.") + .build(); + return Flux.just(ServerSentEvent.builder(errorResponse) + .event("error") + .build()); + } + + log.info("Starting streaming conversation: {}", conversationId); + ChatConversation conversation = conversationDao.findById(conversationId) + .orElseThrow(() -> new IllegalArgumentException("Conversation not found: " + conversationId)); + + // Manually load messages for conversation history + List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId); + conversation.setMessages(messages); + + if (conversation.getTitle().startsWith("conversation")) { + // Auto-generate title from first user message + String title = message.length() > 30 ? message.substring(0, 27) + "..." : message; + conversation.setTitle(title); + conversationDao.save(conversation); + } + + // Add user message to conversation + ChatMessage chatMessage = ChatMessage.builder() + .conversationId(conversationId) + .content(message) + .role("user") + .build(); + chatMessage = messageDao.save(chatMessage); + + ChatRequestContext context = ChatRequestContext.builder() + .message(message) + .conversationId(conversationId) + .conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null + : conversation.getMessages().subList(0, conversation.getMessages().size() - 1)) + .build(); + + // Stream response from AI service + StringBuilder fullResponse = new StringBuilder(); + ChatMessage finalChatMessage = chatMessage; + return chatClientProviderService.streamChat(context) + .map(chunk -> { + fullResponse.append(chunk); + ChatResponseChunk responseChunk = ChatResponseChunk.builder() + .conversationId(conversationId) + .userMessageId(finalChatMessage.getId()) + .response(chunk) + .build(); + + return ServerSentEvent.builder(responseChunk) + .event("message") + .build(); + }) + .concatWith(Flux.defer(() -> { + // Add the complete AI response to conversation + ChatMessage assistantMessage = ChatMessage.builder() + .conversationId(conversationId) + .content(fullResponse.toString()) + .role("assistant") + .build(); + assistantMessage = messageDao.save(assistantMessage); + ChatResponseChunk finalResponse = ChatResponseChunk.builder() + .conversationId(conversationId) + .response("") + .assistantMessageId(assistantMessage.getId()) + .build(); + + return Flux.just(ServerSentEvent.builder(finalResponse) + .event("complete") + .build()); + })) + .doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId)) + .doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, + error.getMessage(), error)) + .onErrorResume(error -> { + ChatResponseChunk errorResponse = ChatResponseChunk.builder() + .conversationId(conversationId) + .response("An error occurred: " + error.getMessage()) + .userMessageId(finalChatMessage.getId()) + .build(); + return Flux.just(ServerSentEvent.builder(errorResponse) + .event("error") + .build()); + }); + } + + @Override + public ChatConversation createConversation() { + ChatConversation conversation = new ChatConversation(); + conversation.setTitle("conversation-" + UUID.randomUUID().toString().substring(0, 4)); + return conversationDao.save(conversation); + } + + @Override + public ChatConversation getConversation(Long conversationId) { + if (conversationId == null) { + return null; + } + ChatConversation conversation = conversationDao.findById(conversationId).orElse(null); + if (conversation != null) { + List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId); + conversation.setMessages(messages); + } + return conversation; + } + + @Override + public List<ChatConversation> getAllConversations() { + List<ChatConversation> conversations = conversationDao.findAll(Sort.by(Sort.Direction.DESC, "id")); + if (conversations.isEmpty()) { + return conversations; + } + List<Long> conversationIds = conversations.stream() + .map(ChatConversation::getId) + .toList(); + List<ChatMessage> allMessages = messageDao.findByConversationIdInOrderByGmtCreateAsc(conversationIds); + Map<Long, List<ChatMessage>> messagesByConversationId = allMessages.stream() + .collect(Collectors.groupingBy(ChatMessage::getConversationId)); + for (ChatConversation conversation : conversations) { + List<ChatMessage> messages = messagesByConversationId.getOrDefault(conversation.getId(), + Collections.emptyList()); + conversation.setMessages(messages); + } + return conversations; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteConversation(Long conversationId) { + List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId); + if (!messages.isEmpty()) { + messageDao.deleteAll(messages); + } + conversationDao.deleteById(conversationId); + } + + @Override + public Boolean saveSecurityData(SecurityData securityData) { + Optional<ChatConversation> chatConversation = conversationDao.findById(securityData.getConversationId()); + if (chatConversation.isPresent()) { + ChatConversation conversation = chatConversation.get(); + conversation.setSecurityData(AesUtil.aesEncode(securityData.getSecurityData())); + conversationDao.save(conversation); + return true; + } + return false; + } + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/McpServerServiceImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/McpServerServiceImpl.java new file mode 100644 index 0000000..23c0dda --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/McpServerServiceImpl.java @@ -0,0 +1,66 @@ +/* + * 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 org.apache.hertzbeat.ai.service.McpServerService; +import org.apache.hertzbeat.ai.tools.AlertDefineTools; +import org.apache.hertzbeat.ai.tools.AlertTools; +import org.apache.hertzbeat.ai.tools.MetricsTools; +import org.apache.hertzbeat.ai.tools.MonitorTools; +import org.apache.hertzbeat.ai.tools.ScheduleTools; +import org.apache.hertzbeat.ai.tools.SkillTools; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.method.MethodToolCallbackProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Implementation of the McpServerService interface. + * This service provides functionality for handling MCP server operations. + */ +@Service +@Configuration +public class McpServerServiceImpl implements McpServerService { + @Autowired + private MonitorTools monitorTools; + @Autowired + private AlertTools alertTools; + @Autowired + private MetricsTools metricsTools; + @Autowired + private AlertDefineTools alertDefineTools; + @Autowired + private SkillTools skillTools; + @Autowired + private ScheduleTools scheduleTools; + + @Bean + public ToolCallbackProvider hertzbeatTools() { + // Note: DatabaseTools is NOT exposed to AI directly. + // AI should use Skills (via skillTools) to perform database diagnostics. + return MethodToolCallbackProvider.builder() + .toolObjects(monitorTools, alertTools, alertDefineTools, metricsTools, + skillTools, scheduleTools) + .build(); + } +} + + diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/SopScheduleServiceImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/SopScheduleServiceImpl.java new file mode 100644 index 0000000..88c8d0a --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/SopScheduleServiceImpl.java @@ -0,0 +1,157 @@ +/* + * 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 java.time.LocalDateTime; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.dao.SopScheduleDao; +import org.apache.hertzbeat.ai.service.SopScheduleService; +import org.apache.hertzbeat.common.entity.ai.SopSchedule; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.support.CronExpression; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Implementation of SopScheduleService for managing scheduled SOP executions. + */ +@Slf4j +@Service +public class SopScheduleServiceImpl implements SopScheduleService { + + private final SopScheduleDao sopScheduleDao; + + @Autowired + public SopScheduleServiceImpl(SopScheduleDao sopScheduleDao) { + this.sopScheduleDao = sopScheduleDao; + } + + @Override + @Transactional + public SopSchedule createSchedule(SopSchedule schedule) { + // Validate cron expression + validateCronExpression(schedule.getCronExpression()); + + // Calculate next run time + schedule.setNextRunTime(calculateNextRunTime(schedule.getCronExpression())); + schedule.setEnabled(schedule.getEnabled() != null ? schedule.getEnabled() : true); + + SopSchedule saved = sopScheduleDao.save(schedule); + log.info("Created schedule {} for conversation {} with SOP {}", + saved.getId(), saved.getConversationId(), saved.getSopName()); + return saved; + } + + @Override + @Transactional + public SopSchedule updateSchedule(SopSchedule schedule) { + SopSchedule existing = sopScheduleDao.findById(schedule.getId()) + .orElseThrow(() -> new IllegalArgumentException("Schedule not found: " + schedule.getId())); + + // Update fields + existing.setSopName(schedule.getSopName()); + existing.setSopParams(schedule.getSopParams()); + + // If cron expression changed, recalculate next run time + if (!existing.getCronExpression().equals(schedule.getCronExpression())) { + validateCronExpression(schedule.getCronExpression()); + existing.setCronExpression(schedule.getCronExpression()); + existing.setNextRunTime(calculateNextRunTime(schedule.getCronExpression())); + } + + if (schedule.getEnabled() != null) { + existing.setEnabled(schedule.getEnabled()); + } + + return sopScheduleDao.save(existing); + } + + @Override + @Transactional + public void deleteSchedule(Long id) { + log.info("Deleting schedule {}", id); + sopScheduleDao.deleteById(id); + } + + @Override + public SopSchedule getSchedule(Long id) { + return sopScheduleDao.findById(id).orElse(null); + } + + @Override + public List<SopSchedule> getSchedulesByConversation(Long conversationId) { + return sopScheduleDao.findByConversationId(conversationId); + } + + @Override + @Transactional + public SopSchedule toggleSchedule(Long id, boolean enabled) { + SopSchedule schedule = sopScheduleDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Schedule not found: " + id)); + + schedule.setEnabled(enabled); + + // If enabling, recalculate next run time + if (enabled) { + schedule.setNextRunTime(calculateNextRunTime(schedule.getCronExpression())); + } + + log.info("Schedule {} {} ", id, enabled ? "enabled" : "disabled"); + return sopScheduleDao.save(schedule); + } + + @Override + public List<SopSchedule> getDueSchedules() { + return sopScheduleDao.findDueSchedules(LocalDateTime.now()); + } + + @Override + @Transactional + public void updateAfterExecution(Long id) { + SopSchedule schedule = sopScheduleDao.findById(id).orElse(null); + if (schedule == null) { + return; + } + + schedule.setLastRunTime(LocalDateTime.now()); + schedule.setNextRunTime(calculateNextRunTime(schedule.getCronExpression())); + sopScheduleDao.save(schedule); + + log.debug("Updated schedule {} - Last run: {}, Next run: {}", + id, schedule.getLastRunTime(), schedule.getNextRunTime()); + } + + private void validateCronExpression(String cronExpression) { + try { + CronExpression.parse(cronExpression); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid cron expression: " + cronExpression, e); + } + } + + private LocalDateTime calculateNextRunTime(String cronExpression) { + try { + CronExpression cron = CronExpression.parse(cronExpression); + return cron.next(LocalDateTime.now()); + } catch (Exception e) { + log.error("Failed to calculate next run time for cron: {}", cronExpression, e); + return null; + } + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngine.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngine.java new file mode 100644 index 0000000..4fccb2a --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngine.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.ai.sop.engine; + +import java.util.Map; +import org.apache.hertzbeat.ai.sop.model.SopDefinition; +import org.apache.hertzbeat.ai.sop.model.SopResult; +import reactor.core.publisher.Flux; + +/** + * Engine for executing AI SOPs. + */ +public interface SopEngine { + + /** + * Execute an SOP with the given input parameters (streaming mode). + * @param definition The SOP definition to execute. + * @param inputParams Input parameters for the SOP. + * @return A stream of execution logs/results. + */ + Flux<String> execute(SopDefinition definition, Map<String, Object> inputParams); + + /** + * Execute an SOP synchronously and return a unified result. + * Used for AI tool calls and programmatic access. + * @param definition The SOP definition to execute. + * @param inputParams Input parameters for the SOP. + * @return Unified SOP execution result. + */ + SopResult executeSync(SopDefinition definition, Map<String, Object> inputParams); +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngineImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngineImpl.java new file mode 100644 index 0000000..44ac80d --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngineImpl.java @@ -0,0 +1,293 @@ +/* + * 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 java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.sop.executor.SopExecutor; +import org.apache.hertzbeat.ai.sop.model.OutputConfig; +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.ai.sop.model.SopStep; +import org.apache.hertzbeat.ai.sop.model.StepResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +/** + * Implementation of the SopEngine. + */ +@Slf4j +@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 + public SopEngineImpl(List<SopExecutor> executors) { + this.executors = executors; + } + + @Override + public Flux<String> execute(SopDefinition definition, Map<String, Object> inputParams) { + return Flux.create(sink -> { + try { + 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()); + + // Find appropriate executor + SopExecutor executor = findExecutor(step.getType()); + if (executor == null) { + String error = "No executor found for step type: " + step.getType(); + log.error(error); + sink.error(new IllegalArgumentException(error)); + return; + } + + // Execute the step + try { + Object result = executor.execute(step, context); + context.put(step.getId(), result); + + // For LLM steps, output the result (the report) + if ("llm".equalsIgnoreCase(step.getType()) && result != null) { + sink.next("--- Report from " + step.getId() + " ---"); + sink.next(String.valueOf(result)); + sink.next("--- End of Report ---"); + } + + sink.next("Step " + step.getId() + " completed"); + } catch (Exception e) { + log.error("Error executing step {}: {}", step.getId(), e.getMessage(), e); + sink.error(e); + return; + } + } + + sink.next("SOP " + definition.getName() + " completed successfully."); + sink.complete(); + } catch (Exception e) { + log.error("Error executing SOP {}: {}", definition.getName(), e.getMessage(), e); + sink.error(e); + } finally { + CONTEXT_BUS.remove(); + } + }); + } + + @Override + 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()) + .startTime(startTime); + + resultBuilder.outputType(outputConfig.getOutputType()); + resultBuilder.outputFormat(outputConfig.getFormat() != null ? outputConfig.getFormat() : "text"); + resultBuilder.language(outputConfig.getLanguageCode()); + + try { + log.info("Starting sync execution of SOP: {}", definition.getName()); + + for (SopStep step : definition.getSteps()) { + long stepStartTime = System.currentTimeMillis(); + StepResult.StepResultBuilder stepBuilder = StepResult.builder() + .stepId(step.getId()) + .type(step.getType()) + .startTime(stepStartTime); + + // Find appropriate executor + SopExecutor executor = findExecutor(step.getType()); + if (executor == null) { + String error = "No executor found for step type: " + step.getType(); + log.error(error); + stepBuilder.status("FAILED") + .error(error) + .endTime(System.currentTimeMillis()); + stepResults.add(stepBuilder.build()); + + return buildFailedResult(resultBuilder, stepResults, error, startTime); + } + + // Execute the step + try { + Object result = executor.execute(step, context); + context.put(step.getId(), result); + + long stepEndTime = System.currentTimeMillis(); + stepBuilder.status("SUCCESS") + .output(result) + .endTime(stepEndTime) + .duration(stepEndTime - stepStartTime); + stepResults.add(stepBuilder.build()); + + } catch (Exception e) { + log.error("Error executing step {}: {}", step.getId(), e.getMessage(), e); + long stepEndTime = System.currentTimeMillis(); + stepBuilder.status("FAILED") + .error(e.getMessage()) + .endTime(stepEndTime) + .duration(stepEndTime - stepStartTime); + stepResults.add(stepBuilder.build()); + + return buildFailedResult(resultBuilder, stepResults, e.getMessage(), startTime); + } + } + + // Build successful result + long endTime = System.currentTimeMillis(); + resultBuilder.status("SUCCESS") + .endTime(endTime) + .duration(endTime - startTime) + .steps(stepResults) + .data(context); + + // Extract summary and content based on output configuration + extractOutputContent(resultBuilder, context, outputConfig, stepResults); + + return resultBuilder.build(); + + } catch (Exception e) { + log.error("Error executing SOP {}: {}", definition.getName(), e.getMessage(), e); + return buildFailedResult(resultBuilder, stepResults, e.getMessage(), startTime); + } + } + + private SopResult buildFailedResult(SopResult.SopResultBuilder builder, + List<StepResult> stepResults, String error, long startTime) { + long endTime = System.currentTimeMillis(); + return builder.status("FAILED") + .endTime(endTime) + .duration(endTime - startTime) + .steps(stepResults) + .error(error) + .summary("SOP execution failed: " + error) + .build(); + } + + private void extractOutputContent(SopResult.SopResultBuilder builder, + Map<String, Object> context, OutputConfig outputConfig, List<StepResult> stepResults) { + + // Extract content from specified step or last LLM step + String contentStepId = outputConfig.getContentStep(); + String summaryStepId = outputConfig.getSummaryStep(); + + // Find content + if (contentStepId != null && context.containsKey(contentStepId)) { + builder.content(String.valueOf(context.get(contentStepId))); + } else { + // Default: use the last LLM step's output as content + for (int i = stepResults.size() - 1; i >= 0; i--) { + StepResult step = stepResults.get(i); + if ("llm".equalsIgnoreCase(step.getType()) && step.getOutput() != null) { + builder.content(String.valueOf(step.getOutput())); + break; + } + } + } + + // Generate summary based on output type + OutputType outputType = outputConfig.getOutputType(); + switch (outputType) { + case REPORT: + builder.summary("Report generated successfully"); + break; + case DATA: + builder.summary("Data retrieved: " + context.size() + " items"); + break; + case ACTION: + builder.summary("Action pending confirmation"); + break; + default: + builder.summary("Operation completed successfully"); + } + + // Override with custom summary step if specified + if (summaryStepId != null && context.containsKey(summaryStepId)) { + String summaryContent = String.valueOf(context.get(summaryStepId)); + // Take first line as summary + int newlineIndex = summaryContent.indexOf('\n'); + if (newlineIndex > 0 && newlineIndex < 200) { + builder.summary(summaryContent.substring(0, newlineIndex)); + } else if (summaryContent.length() > 200) { + builder.summary(summaryContent.substring(0, 200) + "..."); + } else { + builder.summary(summaryContent); + } + } + } + + private SopExecutor findExecutor(String type) { + for (SopExecutor executor : executors) { + if (executor.support(type)) { + return executor; + } + } + return null; + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/LlmExecutor.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/LlmExecutor.java new file mode 100644 index 0000000..1efaa2e --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/LlmExecutor.java @@ -0,0 +1,110 @@ +/* + * 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.executor; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext; +import org.apache.hertzbeat.ai.service.ChatClientProviderService; +import org.apache.hertzbeat.ai.sop.model.SopStep; +import org.apache.hertzbeat.ai.utils.SopMessageUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * Executor for 'llm' type steps. + * Calls the AI model for reasoning or summarization. + * Reuses the existing ChatClientProviderService for LLM interaction. + */ +@Slf4j +@Component +public class LlmExecutor implements SopExecutor { + + private final ChatClientProviderService chatClientProviderService; + + @Autowired + public LlmExecutor(ChatClientProviderService chatClientProviderService) { + this.chatClientProviderService = chatClientProviderService; + } + + @Override + public boolean support(String type) { + return "llm".equalsIgnoreCase(type); + } + + @Override + public Object execute(SopStep step, Map<String, Object> context) { + String prompt = step.getPrompt(); + if (prompt == null || prompt.trim().isEmpty()) { + throw new IllegalArgumentException("LLM step must have a 'prompt' field"); + } + + // Check if LLM is configured + if (!chatClientProviderService.isConfigured()) { + log.warn("LLM provider is not configured, returning mock response"); + return "Mock LLM response: Provider not configured"; + } + + // Resolve variables in prompt from context + String resolvedPrompt = resolvePrompt(prompt, context); + + // Add language instruction based on configuration + String language = (String) context.getOrDefault("_language", "zh"); + resolvedPrompt = addLanguageInstruction(resolvedPrompt, language); + + log.info("Executing LLM step with prompt length: {}", resolvedPrompt.length()); + + try { + // Build chat request context + ChatRequestContext chatContext = ChatRequestContext.builder() + .message(resolvedPrompt) + .build(); + + // Use existing service to get response (collect stream to string) + StringBuilder responseBuilder = new StringBuilder(); + chatClientProviderService.streamChat(chatContext) + .doOnNext(responseBuilder::append) + .blockLast(); + + String response = responseBuilder.toString(); + log.debug("LLM response length: {}", response.length()); + return response; + } catch (Exception e) { + log.error("Failed to execute LLM step: {}", e.getMessage()); + throw new RuntimeException("LLM execution failed", e); + } + } + + private String addLanguageInstruction(String prompt, String language) { + String langInstruction = SopMessageUtil.getMessage("sop.llm.language.instruction", language); + return "\n\n[" + langInstruction + "]\n\n" + prompt; + } + + private String resolvePrompt(String prompt, Map<String, Object> context) { + // Simple variable replacement: ${variable} -> context.get("variable") + String result = prompt; + for (Map.Entry<String, Object> entry : context.entrySet()) { + String placeholder = "${" + entry.getKey() + "}"; + if (result.contains(placeholder)) { + result = result.replace(placeholder, String.valueOf(entry.getValue())); + } + } + return result; + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/SopExecutor.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/SopExecutor.java new file mode 100644 index 0000000..5826ad2 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/SopExecutor.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.ai.sop.executor; + +import org.apache.hertzbeat.ai.sop.model.SopStep; + +import java.util.Map; + +/** + * Interface for SOP step executors. + */ +public interface SopExecutor { + + /** + * Check if this executor supports the given step type. + * @param type Step type. + * @return true if supported. + */ + boolean support(String type); + + /** + * Execute the given step. + * @param step The step to execute. + * @param context Current execution context. + * @return Execution result. + */ + Object execute(SopStep step, Map<String, Object> context); +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/ToolExecutor.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/ToolExecutor.java new file mode 100644 index 0000000..4e7c0ce --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/ToolExecutor.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.ai.sop.executor; + +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.sop.model.SopStep; +import org.apache.hertzbeat.ai.sop.registry.ToolRegistry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +/** + * Executor for 'tool' type steps. + * Uses ToolRegistry to dynamically discover and invoke @Tool annotated methods. + */ +@Slf4j +@Component +public class ToolExecutor implements SopExecutor { + + private final ToolRegistry toolRegistry; + + @Autowired + public ToolExecutor(@Lazy ToolRegistry toolRegistry) { + this.toolRegistry = toolRegistry; + } + + @Override + public boolean support(String type) { + return "tool".equalsIgnoreCase(type); + } + + @Override + public Object execute(SopStep step, Map<String, Object> context) { + String toolName = step.getTool(); + log.info("Executing tool step: {}", toolName); + + Map<String, Object> args = resolveArgs(step.getArgs(), context); + + try { + String result = toolRegistry.invoke(toolName, args); + log.debug("Tool {} returned result length: {}", toolName, + result != null ? result.length() : 0); + return result; + } catch (Exception e) { + log.error("Failed to execute tool {}: {}", toolName, e.getMessage()); + throw new RuntimeException("Tool execution failed: " + toolName, e); + } + } + + private Map<String, Object> resolveArgs(Map<String, Object> args, Map<String, Object> context) { + if (args == null) { + return new HashMap<>(); + } + + Map<String, Object> resolved = new HashMap<>(); + for (Map.Entry<String, Object> entry : args.entrySet()) { + Object value = entry.getValue(); + if (value instanceof String) { + String strValue = (String) value; + for (Map.Entry<String, Object> ctxEntry : context.entrySet()) { + String placeholder = "${" + ctxEntry.getKey() + "}"; + if (strValue.contains(placeholder)) { + strValue = strValue.replace(placeholder, String.valueOf(ctxEntry.getValue())); + } + } + resolved.put(entry.getKey(), strValue); + } else { + resolved.put(entry.getKey(), value); + } + } + return resolved; + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputConfig.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputConfig.java new file mode 100644 index 0000000..c99bd93 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputConfig.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.ai.sop.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Output configuration for SOP definition. + * Defines how the SOP result should be formatted and presented. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OutputConfig { + + /** + * Output type: report, simple, data, action + */ + private String type; + + /** + * Output format: markdown, json, text + */ + private String format; + + /** + * Which step's output should be used as the final summary + */ + private String summaryStep; + + /** + * Which step's output should be used as the main content + */ + private String contentStep; + + /** + * Language for LLM responses and SOP output: zh (Chinese), en (English) + * Default is zh (Chinese) + */ + private String language; + + /** + * Get language code. + * If not specified in skill config, uses system default locale (from user's SystemConfig). + */ + public String getLanguageCode() { + if (language == null || language.isEmpty()) { + // Use system default locale (set by user's SystemConfig) + java.util.Locale defaultLocale = java.util.Locale.getDefault(); + if (defaultLocale.getLanguage().equals("en")) { + return "en"; + } + return "zh"; // Default to Chinese for non-English locales + } + return language.toLowerCase(); + } + + /** + * Check if the language is Chinese + */ + public boolean isChinese() { + return "zh".equals(getLanguageCode()) || "chinese".equalsIgnoreCase(language); + } + + /** + * Get the OutputType enum from string + */ + public OutputType getOutputType() { + if (type == null) { + return OutputType.SIMPLE; + } + try { + return OutputType.valueOf(type.toUpperCase()); + } catch (IllegalArgumentException e) { + return OutputType.SIMPLE; + } + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputType.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputType.java new file mode 100644 index 0000000..f8eb05d --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputType.java @@ -0,0 +1,44 @@ +/* + * 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.model; + +/** + * SOP output type enumeration. + * Defines different types of SOP execution results. + */ +public enum OutputType { + /** + * Report type - generates a detailed report (e.g., daily inspection, fault analysis) + */ + REPORT, + + /** + * Simple type - returns simple success/failure result (e.g., restart, clear cache) + */ + SIMPLE, + + /** + * Data type - returns structured data (e.g., query resources, statistics) + */ + DATA, + + /** + * Action type - returns pending actions that require human confirmation + */ + ACTION +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopDefinition.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopDefinition.java new file mode 100644 index 0000000..24e0b36 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopDefinition.java @@ -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.ai.sop.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Model representing an AI SOP (Standard Operating Procedure) definition. + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SopDefinition { + + /** + * Unique name of the SOP skill. + */ + private String name; + + /** + * Description of what this SOP does (used by AI for discovery). + */ + private String description; + + /** + * Version of the SOP. + */ + private String version; + + /** + * Input parameters required by this SOP. + */ + private List<SopParameter> parameters; + + /** + * Ordered list of steps to execute. + */ + private List<SopStep> steps; + + /** + * Output configuration for this SOP. + */ + private OutputConfig output; +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopParameter.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopParameter.java new file mode 100644 index 0000000..55eb75f --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopParameter.java @@ -0,0 +1,58 @@ +/* + * 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.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Model representing an input parameter for an AI SOP. + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SopParameter { + + /** + * Name of the parameter. + */ + private String name; + + /** + * Type of the parameter (string, long, boolean, etc.). + */ + private String type; + + /** + * Description of the parameter. + */ + private String description; + + /** + * Whether the parameter is required. + */ + private boolean required; + + /** + * Default value if not provided. + */ + private String defaultValue; +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopResult.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopResult.java new file mode 100644 index 0000000..13ad85b --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopResult.java @@ -0,0 +1,197 @@ +/* + * 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.model; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.ai.utils.SopMessageUtil; + +/** + * Unified SOP execution result. + * Contains all information about a SOP execution including metadata, output, and step details. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SopResult { + + // ===== Metadata ===== + + /** + * SOP name + */ + private String sopName; + + /** + * SOP version + */ + private String sopVersion; + + /** + * Execution status: SUCCESS, FAILED, PARTIAL + */ + private String status; + + /** + * Start timestamp in milliseconds + */ + private long startTime; + + /** + * End timestamp in milliseconds + */ + private long endTime; + + /** + * Duration in milliseconds + */ + private long duration; + + // ===== Output Type ===== + + /** + * Output type: REPORT, SIMPLE, DATA, ACTION + */ + private OutputType outputType; + + /** + * Output format: markdown, json, text + */ + private String outputFormat; + + /** + * Language code: zh (Chinese), en (English) + */ + @Builder.Default + private String language = "zh"; + + // ===== Output Content ===== + + /** + * Short summary (one line) + */ + private String summary; + + /** + * Main content (Markdown report / JSON data / etc.) + */ + private String content; + + /** + * Structured data (for programmatic processing) + */ + @Builder.Default + private Map<String, Object> data = new HashMap<>(); + + // ===== Step Details ===== + + /** + * Execution result of each step + */ + @Builder.Default + private List<StepResult> steps = new ArrayList<>(); + + /** + * Error message if failed + */ + private String error; + + /** + * Get localized message using i18n. + */ + private String msg(String code) { + return SopMessageUtil.getMessage(code, language); + } + + /** + * Convert to AI-friendly response format. + * Used when SOP is called as a tool by AI. + */ + public String toAiResponse() { + StringBuilder sb = new StringBuilder(); + + sb.append(msg("sop.result.title")).append(": ").append(status).append("\n"); + sb.append(msg("sop.result.name")).append(": ").append(sopName) + .append(" (v").append(sopVersion).append(")\n"); + sb.append(msg("sop.result.duration")).append(": ").append(duration).append("ms\n"); + + if (summary != null && !summary.isEmpty()) { + sb.append(msg("sop.result.summary")).append(": ").append(summary).append("\n"); + } + + if ("FAILED".equals(status) && error != null) { + sb.append(msg("sop.result.error")).append(": ").append(error).append("\n"); + return sb.toString(); + } + + if (outputType == OutputType.REPORT && content != null) { + sb.append("\n--- ").append(msg("sop.result.report.title")).append(" ---\n"); + sb.append(content); + } else if (outputType == OutputType.DATA && !data.isEmpty()) { + sb.append("\n").append(msg("sop.result.data.title")).append(":\n"); + for (Map.Entry<String, Object> entry : data.entrySet()) { + sb.append("- ").append(entry.getKey()).append(": ").append(entry.getValue()).append("\n"); + } + } else if (outputType == OutputType.SIMPLE) { + sb.append(msg("sop.result.simple.complete")).append("\n"); + } + + return sb.toString(); + } + + /** + * Convert to SSE stream format for real-time updates. + */ + public String toSseFormat() { + StringBuilder sb = new StringBuilder(); + sb.append("event: sop_result\n"); + sb.append("data: {"); + sb.append("\"sopName\":\"").append(sopName).append("\","); + sb.append("\"status\":\"").append(status).append("\","); + sb.append("\"duration\":").append(duration).append(","); + sb.append("\"outputType\":\"").append(outputType).append("\","); + sb.append("\"language\":\"").append(language).append("\","); + if (summary != null) { + sb.append("\"summary\":\"").append(escapeJson(summary)).append("\","); + } + if (content != null) { + sb.append("\"content\":\"").append(escapeJson(content)).append("\","); + } + sb.append("\"stepsCount\":").append(steps.size()); + sb.append("}\n\n"); + return sb.toString(); + } + + private String escapeJson(String text) { + if (text == null) { + return ""; + } + return text.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopStep.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopStep.java new file mode 100644 index 0000000..95f5e69 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopStep.java @@ -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.ai.sop.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * Model representing a single step in an AI SOP. + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SopStep { + + /** + * Unique ID of the step within the SOP. + */ + private String id; + + /** + * Type of executor: 'tool', 'llm', 'script', etc. + */ + private String type; + + /** + * Name of the tool to call (if type is 'tool'). + */ + private String tool; + + /** + * Arguments for the step (supports ${var} expressions). + */ + private Map<String, Object> args; + + /** + * Prompt template (if type is 'llm'). + */ + private String prompt; + + /** + * Condition to execute this step (optional). + */ + private String condition; +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/StepResult.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/StepResult.java new file mode 100644 index 0000000..a3c43fb --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/StepResult.java @@ -0,0 +1,73 @@ +/* + * 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.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents the result of a single SOP step execution. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StepResult { + + /** + * Step ID + */ + private String stepId; + + /** + * Step type (tool, llm, condition, etc.) + */ + private String type; + + /** + * Execution status: SUCCESS, FAILED, SKIPPED + */ + private String status; + + /** + * Start timestamp in milliseconds + */ + private long startTime; + + /** + * End timestamp in milliseconds + */ + private long endTime; + + /** + * Duration in milliseconds + */ + private long duration; + + /** + * Step output/result + */ + private Object output; + + /** + * Error message if failed + */ + private String error; +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SkillRegistry.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SkillRegistry.java new file mode 100644 index 0000000..ec508c4 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SkillRegistry.java @@ -0,0 +1,103 @@ +/* + * 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 lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.sop.engine.SopEngine; +import org.apache.hertzbeat.ai.sop.model.SopDefinition; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Registry for AI SOP skills. + * Manages the lifecycle of SOPs and exposes them as Spring AI tools. + */ +@Slf4j +@Service +@Configuration +public class SkillRegistry implements ToolCallbackProvider { + + private final SopYamlLoader yamlLoader; + private final SopEngine sopEngine; + private final Map<String, SopDefinition> skillMap = new ConcurrentHashMap<>(); + + @Autowired + public SkillRegistry(SopYamlLoader yamlLoader, SopEngine sopEngine) { + this.yamlLoader = yamlLoader; + this.sopEngine = sopEngine; + } + + @PostConstruct + public void init() { + refreshSkills(); + } + + /** + * Reload all skills from YAML files. + */ + public void refreshSkills() { + List<SopDefinition> loadedSkills = yamlLoader.loadAllSkills(); + skillMap.clear(); + for (SopDefinition skill : loadedSkills) { + skillMap.put(skill.getName(), skill); + } + log.info("SkillRegistry initialized with {} skills", skillMap.size()); + } + + /** + * Get a specific SOP definition by name. + * @param name Skill name. + * @return SopDefinition or null if not found. + */ + public SopDefinition getSkill(String name) { + return skillMap.get(name); + } + + /** + * Get all registered SOP definitions. + * @return Collection of all SopDefinitions. + */ + public List<SopDefinition> getAllSkills() { + return new ArrayList<>(skillMap.values()); + } + + /** + * Provides the SOP skills as ToolCallbacks for Spring AI. + * @return Array of ToolCallbacks. + */ + @Override + public ToolCallback[] getToolCallbacks() { + List<ToolCallback> callbacks = new ArrayList<>(); + + for (SopDefinition skill : skillMap.values()) { + log.debug("Registering SOP as tool: {}", skill.getName()); + callbacks.add(new SopToolCallback(skill, sopEngine)); + } + + return callbacks.toArray(new ToolCallback[0]); + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SopToolCallback.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SopToolCallback.java new file mode 100644 index 0000000..90ce21e --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SopToolCallback.java @@ -0,0 +1,100 @@ +/* + * 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 org.apache.hertzbeat.ai.sop.engine.SopEngine; +import org.apache.hertzbeat.ai.sop.model.SopDefinition; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.definition.ToolDefinition; + +/** + * ToolCallback implementation for executing AI SOPs. + * Wraps a SOP definition as a Spring AI tool. + */ +public class SopToolCallback implements ToolCallback { + + private final SopDefinition definition; + private final SopEngine sopEngine; + private final ToolDefinition toolDefinition; + + public SopToolCallback(SopDefinition definition, SopEngine sopEngine) { + this.definition = definition; + this.sopEngine = sopEngine; + this.toolDefinition = ToolDefinition.builder() + .name(definition.getName()) + .description(definition.getDescription()) + .inputSchema(buildInputSchema()) + .build(); + } + + @Override + public ToolDefinition getToolDefinition() { + return toolDefinition; + } + + @Override + public String call(String arguments) { + // TODO: Parse arguments and execute SOP + return "SOP " + definition.getName() + " execution started."; + } + + 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(","); + } + 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("\""); + } + schema.append("}"); + first = false; + } + } + + schema.append("}}"); + return schema.toString(); + } + + private String mapType(String type) { + if (type == null) { + return "string"; + } + switch (type.toLowerCase()) { + case "boolean": + return "boolean"; + case "integer": + case "int": + case "long": + return "integer"; + case "number": + case "double": + case "float": + return "number"; + default: + return "string"; + } + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SopYamlLoader.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SopYamlLoader.java new file mode 100644 index 0000000..7e37e8f --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SopYamlLoader.java @@ -0,0 +1,75 @@ +/* + * 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 tools.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.sop.model.SopDefinition; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import tools.jackson.dataformat.yaml.YAMLFactory; + +/** + * Loader for AI SOP definitions from YAML files. + */ +@Slf4j +@Service +public class SopYamlLoader { + + private static final String SKILLS_PATH_PATTERN = "classpath:skills/*.yml"; + private final ObjectMapper yamlMapper; + + public SopYamlLoader() { + this.yamlMapper = new ObjectMapper(new YAMLFactory()); + } + + /** + * Load all SOP definitions from the classpath. + * @return List of loaded SopDefinition objects. + */ + public List<SopDefinition> loadAllSkills() { + List<SopDefinition> skills = new ArrayList<>(); + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + + try { + Resource[] resources = resolver.getResources(SKILLS_PATH_PATTERN); + log.info("Found {} SOP definition files", resources.length); + + for (Resource resource : resources) { + try { + SopDefinition definition = yamlMapper.readValue(resource.getInputStream(), SopDefinition.class); + if (definition != null) { + skills.add(definition); + log.info("Loaded SOP skill: {}", definition.getName()); + } + } catch (Exception e) { + log.error("Failed to parse SOP definition from {}: {}", resource.getFilename(), e.getMessage()); + } + } + } catch (IOException e) { + log.error("Failed to scan for SOP definition files: {}", e.getMessage()); + } + + return skills; + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/ToolRegistry.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/ToolRegistry.java new file mode 100644 index 0000000..77f3a68 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/ToolRegistry.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.ai.sop.registry; + + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * Registry for all @Tool annotated methods. + * Automatically discovers and registers tool methods on first use. + */ +@Slf4j +@Component +public class ToolRegistry { + + private final ApplicationContext applicationContext; + private final Map<String, ToolMethod> tools = new HashMap<>(); + private volatile boolean initialized = false; + + @Autowired + public ToolRegistry(ApplicationContext applicationContext) { + 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(); + initialized = true; + log.info("Registered {} tool methods: {}", tools.size(), tools.keySet()); + } + } + } + } + + 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 void scanBeanForTools(Object bean) { + Class<?> clazz = bean.getClass(); + + // Handle Spring proxies + if (clazz.getName().contains("$$")) { + clazz = clazz.getSuperclass(); + } + + 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()); + } + } + } + + /** + * Invoke a tool by name with the given 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 + + ". Available tools: " + tools.keySet()); + } + return toolMethod.invoke(args); + } + + /** + * Check if a tool exists. + */ + public boolean hasMethod(String toolName) { + ensureInitialized(); + return tools.containsKey(toolName); + } + + /** + * Get all registered tool names. + */ + public Set<String> getToolNames() { + ensureInitialized(); + return 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; + } + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/AlertDefineTools.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/AlertDefineTools.java new file mode 100644 index 0000000..19dbd6c --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/AlertDefineTools.java @@ -0,0 +1,91 @@ +/* + * 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.tools; + +/** + * Tools for alert definition and threshold configuration operations + */ +public interface AlertDefineTools { + + /** + * Create a new alert rule with HertzBeat's expression format based on app hierarchy + + * + * @param name Alert rule name (required, must be unique) + * @param app App name from hierarchy (must match exact hierarchy app value) + * @param metrics Metrics name from hierarchy (must match exact hierarchy metrics value) + * @param fieldConditions Field-specific conditions from metric's children (e.g., "VmName = 'arora'", "total_granted > 1000", + * "total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)") + * @param type Alert rule type: 'realtime' (default) or 'periodic' + * @param period Execution period in seconds (only for periodic rules, default: 300) + * @param times Number of consecutive violations before triggering (default: 3) + * @param priority Alert priority as integer: 0=critical, 1=warning, 2=info (default: 1) + * @param description Alert rule description (optional) + * @param template Alert message template with variables (optional) + * @param datasource Data source type: 'promql' (default) + * @param labels Labels as key:value pairs separated by commas + * @param annotations Annotations as key:value pairs separated by commas + * @param enable Whether to enable the rule immediately (default: true) + * @return Result message with rule ID if successful + */ + String createAlertRule(String name, String app, String metrics, String fieldConditions, + String type, Integer period, Integer times, Integer priority, String description, + String template, String datasource, String labels, String annotations, Boolean enable); + + /** + * List existing alert rules with filtering + * @param search Search term for rule name or description + * @param monitorType Filter by monitor type + * @param enabled Filter by enabled status + * @param pageIndex Page index + * @param pageSize Page size + * @return Formatted list of alert rules + */ + String listAlertRules(String search, String monitorType, Boolean enabled, Integer pageIndex, Integer pageSize); + + /** + * Enable or disable an alert rule + * @param ruleId Alert rule ID + * @param enabled Whether to enable the rule + * @return Result message + */ + String toggleAlertRule(Long ruleId, Boolean enabled); + + /** + * Get detailed information about an alert rule + * @param ruleId Alert rule ID + * @return Detailed rule information + */ + String getAlertRuleDetails(Long ruleId); + + + /** + * Get the hierarchical structure of available apps and metrics for alert rule creation + * @param app App type to get hierarchy for (optional, gets all if not specified) + * @return Hierarchical structure showing apps and their available metrics + */ + String getAppsMetricsHierarchy(String app); + + /** + * Bind monitors to an alert rule by modifying the alert expression + * @param ruleId Alert rule ID to bind monitors to + * @param monitorIds Comma-separated list of monitor IDs to bind + * @return Result message indicating success or failure + */ + String bindMonitorsToAlertRule(Long ruleId, String monitorIds); +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/AlertTools.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/AlertTools.java new file mode 100644 index 0000000..cd8a8fd --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/AlertTools.java @@ -0,0 +1,44 @@ +/* + * 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.tools; + +/** + * Tools for alert operations and alarm data queries + */ +public interface AlertTools { + + /** + * Query alerts with comprehensive filtering and pagination + * @param alertType Alert type (single, group, both) + * @param status Alert status (firing, resolved, all) + * @param search Search term for alert content or labels + * @param sort Sort field (startAt, triggerTimes, status) + * @param order Sort order (asc, desc) + * @param pageIndex Page index + * @param pageSize Page size + * @return Formatted string with alert information + */ + String queryAlerts(String alertType, String status, String search, String sort, String order, Integer pageIndex, Integer pageSize); + + /** + * Get alerts summary statistics + * @return Alert summary information including counts by status + */ + String getAlertsSummary(); + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/DatabaseTools.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/DatabaseTools.java new file mode 100644 index 0000000..272a4f3 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/DatabaseTools.java @@ -0,0 +1,50 @@ +/* + * 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.tools; + +/** + * Database Tools interface for AI-powered database diagnostics. + * Provides predefined safe queries for database health checks and troubleshooting. + */ +public interface DatabaseTools { + + /** + * Get MySQL slow query statistics from performance_schema. + */ + String getMySqlSlowQueries(Long monitorId, Integer limit); + + /** + * Get MySQL current process list. + */ + String getMySqlProcessList(Long monitorId); + + /** + * Get MySQL lock wait information. + */ + String getMySqlLockWaits(Long monitorId); + + /** + * Get MySQL global status variables. + */ + String getMySqlGlobalStatus(Long monitorId, String pattern); + + /** + * Explain a SELECT query for performance analysis. + */ + String explainQuery(Long monitorId, String query); +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MetricsTools.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MetricsTools.java new file mode 100644 index 0000000..45248ab --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MetricsTools.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.ai.tools; + +/** + * Tools for metrics data operations and queries + */ +public interface MetricsTools { + + /** + * Get real-time metrics data for a monitor + * @param monitorId Monitor ID + * @param metrics Metrics name (e.g., "system", "cpu", "memory") + * @return Formatted real-time metrics data + */ + String getRealtimeMetrics(Long monitorId, String metrics); + + /** + * Get historical metrics data for a monitor + * + * @param instance Instance identifier (e.g., "ip:port", "ip", or "domain") + * @param app Monitor type (e.g., "linux", "mysql", "http") + * @param metrics Metrics name (e.g., "system", "cpu", "memory") + * @param metric Specific metric field (e.g., "usage", "used", "available") + * @param label Label filter for specific instances + * @param history Time range (e.g., "1h", "6h", "24h", "7d") + * @param interval Whether to aggregate data with intervals + * @return Historical metrics data formatted for display + */ + String getHistoricalMetrics(String instance, String app, String metrics, String metric, String label, String history, Boolean interval); + + /** + * Check warehouse storage server status + * @return Status of the metrics storage system + */ + String getWarehouseStatus(); + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MonitorTools.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MonitorTools.java new file mode 100644 index 0000000..a821776 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MonitorTools.java @@ -0,0 +1,107 @@ +/* + * 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.tools; + +import java.util.List; + +/** + * Interface for Monitoring Tools + */ +public interface MonitorTools { + + /** + * Add a new monitor with comprehensive configuration + * + * @param name Monitor name + * @param app Monitor type/application (e.g., 'linux', 'mysql', 'http') + * @param intervals Collection interval in seconds (default: 600) + * @param params Monitor-specific parameters as JSON string (e.g., host, port, username, password, etc.) + * @param description Monitor description (optional) + * @return Result message with monitor ID if successful + */ + String addMonitor( + String name, + String app, + Integer intervals, + String params, + String description + ); + + /** + * Add a new monitor with comprehensive configuration, methods specifically for hertzbeat ai + * + * @param conversationId Conversation ID + * @param name Monitor name + * @param app Monitor type/application (e.g., 'linux', 'mysql', 'http') + * @param intervals Collection interval in seconds (default: 600) + * @param params Monitor-specific parameters as JSON string (e.g., host, port, username, password, etc.) + * @param description Monitor description (optional) + * @return Result message with monitor ID if successful + */ + String addMonitorProtected( + Long conversationId, + String name, + String app, + Integer intervals, + String params, + String description + ); + + /** + * List all available monitor types that can be added + * + * @param language Language code for localized names (e.g., 'en-US', 'zh-CN') + * @return Formatted string list of available monitor types with descriptions + */ + String listMonitorTypes(String language); + + /** + * Comprehensive monitor querying with flexible filtering, pagination, and specialized views + * + * @param ids Specific monitor IDs to retrieve (optional) + * @param app Monitor type filter (linux, mysql, http, etc.) + * @param status Monitor status (1=online, 2=offline, 3=unreachable, 0=paused, 9=all) + * @param search Search in monitor names or hosts (partial matching) + * @param labels Label filters, format: 'key1:value1,key2:value2' + * @param sort Sort field (name, gmtCreate, gmtUpdate, status, app) + * @param order Sort order (asc, desc) + * @param pageIndex Page number starting from 0 + * @param pageSize Items per page (1-100 recommended) + * @param includeStats Include status statistics summary + * @return Comprehensive monitor information with optional statistics + */ + String queryMonitors( + List<Long> ids, + String app, + Byte status, + String search, + String labels, + String sort, + String order, + Integer pageIndex, + Integer pageSize, + Boolean includeStats); + + /** + * Get parameter definitions required for a specific monitor type + * + * @param app Monitor type/application name (e.g., 'linux', 'mysql', 'redis') + * @return Formatted string with parameter definitions including field names, types, and requirements + */ + String getMonitorParams(String app); +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/ScheduleTools.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/ScheduleTools.java new file mode 100644 index 0000000..eb4154d --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/ScheduleTools.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.ai.tools; + +/** + * Interface for AI Schedule management tools. + * Allows AI to create, list, and manage scheduled tasks via natural language. + */ +public interface ScheduleTools { + + /** + * Create a scheduled task to execute a skill periodically. + * @param skillName Name of the skill to schedule (e.g., daily_inspection) + * @param cronExpression Cron expression for scheduling (6-digit Spring format) + * @param description Human-readable description of the schedule + * @return Confirmation message with schedule details + */ + String createSchedule(String skillName, String cronExpression, String description); + + /** + * List all scheduled tasks for the current conversation. + * @return List of scheduled tasks with their status + */ + String listSchedules(); + + /** + * Delete a scheduled task by ID. + * @param scheduleId ID of the schedule to delete + * @return Confirmation message + */ + String deleteSchedule(Long scheduleId); + + /** + * Enable or disable a scheduled task. + * @param scheduleId ID of the schedule + * @param enabled Whether to enable or disable + * @return Confirmation message + */ + String toggleSchedule(Long scheduleId, boolean enabled); +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/SkillTools.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/SkillTools.java new file mode 100644 index 0000000..8341619 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/SkillTools.java @@ -0,0 +1,39 @@ +/* + * 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.tools; + +/** + * Interface for AI Skill execution tools. + * Provides methods to list available skills and execute them. + */ +public interface SkillTools { + + /** + * List all available diagnostic skills. + * @return JSON string containing skill metadata (name, description, parameters) + */ + String listSkills(); + + /** + * Execute a diagnostic skill. + * @param skillName Name of the skill to execute + * @param paramsJson JSON string containing skill parameters (e.g., {"monitorId": 123}) + * @return Skill execution result. For report-type skills, returns "[[SKILL_REPORT]]\n{content}" + */ + String executeSkill(String skillName, String paramsJson); +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/AlertDefineToolsImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/AlertDefineToolsImpl.java new file mode 100644 index 0000000..f55bf17 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/AlertDefineToolsImpl.java @@ -0,0 +1,692 @@ +/* + * 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.tools.impl; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; +import com.usthe.sureness.subject.SubjectSum; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.config.McpContextHolder; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.manager.service.AppService; +import org.apache.hertzbeat.ai.tools.AlertDefineTools; +import org.apache.hertzbeat.ai.utils.UtilityClass; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.manager.pojo.dto.Hierarchy; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Implementation of Alert Define Tools functionality + */ +@Slf4j +@Service +public class AlertDefineToolsImpl implements AlertDefineTools { + @Autowired + private AlertDefineService alertDefineService; + @Autowired + private AppService appService; + + @Override + @Tool(name = "create_alert_rule", description = """ + HertzBeat: ALERT RULE means when to alert a user + THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE, + IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'. + APPLY THE SAME LOGIC FOR LESS THAN OPERATOR. + Create a HertzBeat alert rule based on app hierarchy structure and user requirements. + It is important to first understand the hierarchy of apps, metrics, and field conditions + Each app has its own metrics and each metric has its own field conditions. + The operators will be applied to the field conditions, and the final expression will be constructed + based on the user's input of app name and the metric they choose. + CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step + 1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies + 2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions + 3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app + 4. Ask the user to choose a metric from the available metrics + 5. Based on the metric chosen, present the available field conditions/params + 6. You will construct the proper expression with field conditions + 7. once this tool successfully executes, ask the user if they want to bind any existing monitors to this alert rule, + get the monitors list for a particular app using the query_monitors tool. + 8. based on the user's output, conditionally call the bind_monitors_to_alert_rule tool to bind monitors to the alert rule + VERY VERY IMPORTANT: + - ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters + + EXAMPLES FOR FIELD CONDITION EXPRESSION( Do not copy these examples, they are just for reference ): + These are all just examples, you can take inspiration from them and create a rule based on hierarchy, always ask the user for all params, do not assume them, even for these examples: + + 1. Kafka JVM Alert: + - App: "kafka", Metric: "jvm_basic" + - Field condition: equals(VmName, "myVM") + - Field condition expression: equals(VmName, "myVM") + + 2. LLM Credits Alert: + - App: "openai", Metric: "credit_grants" + - Field condition: total_granted > some_value + - Field condition expression: total_granted > 1000 + + 3. HBase Master Alert: + - App: "hbase_master", Metric: "server" + - Field condition: heap_memory_used > 80 or some_factor<100 + - Field condition expression: heap_memory_used > 80 or some_factor<100 + + 4. Complex OpenAI Credits Alert: + - App: "openai", Metric: "credit_grants" + - Field condition: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556) + - Field condition expression: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556) + + FIELD CONDITIONS GUIDANCE: + - Field names come from metric's children in hierarchy (leaf nodes) + - Use the "value" field from the metric's children, not the label when creating conditions + - Supported operators: >, <, >=, <=, ==, !=, exists(), !exists() for numeric fields + - equals(), contains(), matches(),exists(), !equals(), !contains(), !matches(), !exists() for string fields + - Supported logical operators: and, or to connect different field parameter rules or rulesets + - ONLY USE THESE OPERATORS, when creating conditions, do not use any other operators + - Support grouping with parentheses: (condition1 and condition2) or condition3 + - String values should be quoted: equals(VmName, "my-vm") + - Simple conditions: heap_memory_used > 80, total_granted <= 1000 + - Complex conditions: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556) + + PRIORITY LEVELS: + - 0: Critical (immediate action required) + - 1: Warning (attention needed, default) + - 2: Info (informational only) + """) + public String createAlertRule( + @ToolParam(description = "Alert rule name (required, must be unique)", required = true) String name, + @ToolParam(description = "App name from hierarchy (must match exact hierarchy app value)", required = true) String app, + @ToolParam(description = "Metrics name from hierarchy (must match exact hierarchy metrics value)", required = true) String metrics, + @ToolParam(description = "Field conditions expression)", required = true) String fieldConditions, + @ToolParam(description = "Alert rule type: 'realtime' (default) or 'periodic'", required = false) String type, + @ToolParam(description = "Execution period in seconds (only for periodic rules, default: 300)", required = false) Integer period, + @ToolParam(description = "Number of consecutive violations before triggering (default: 3)", required = false) Integer times, + @ToolParam(description = "Alert priority as integer: 0=critical, 1=warning, 2=info (default: 1)", required = false) Integer priority, + @ToolParam(description = "Alert rule description (optional)", required = false) String description, + @ToolParam(description = "Alert message template with variables (optional)", required = false) String template, + @ToolParam(description = "Data source type: 'promql' (default)", required = false) String datasource, + @ToolParam(description = "Labels as key:value pairs separated by commas (e.g., 'env:prod,severity:critical')", required = false) String labels, + @ToolParam(description = "Annotations as key:value pairs separated by commas (e.g., 'summary:High CPU')", required = false) String annotations, + @ToolParam(description = "Whether to enable the rule immediately (default: true)", required = false) Boolean enable) { + + try { + log.info("Creating HertzBeat alert rule: name={}, app={}, metrics={}, fieldConditions={}", name, app, metrics, fieldConditions); + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current subject in create_alert_rule tool: {}", subjectSum); + + // Validate required parameters + if (name == null || name.trim().isEmpty()) { + return "Error: Alert rule name is required"; + } + if (app == null || app.trim().isEmpty()) { + return "Error: App name is required (use get_apps_metrics_hierarchy to find exact names)"; + } + if (metrics == null || metrics.trim().isEmpty()) { + return "Error: Metrics name is required (use get_apps_metrics_hierarchy to find exact names)"; + } + if (fieldConditions == null || fieldConditions.trim().isEmpty()) { + return "Error: Field conditions are required (e.g., 'equals(VmName, \"arora\")', 'total_granted > 1000')"; + } + + // Set defaults + if (type == null || type.trim().isEmpty()) { + type = "realtime"; + } + if (times == null || times <= 0) { + times = 3; + } + if (priority == null) { + priority = 1; // Default to warning + } + if (enable == null) { + enable = true; + } + if (datasource == null || datasource.trim().isEmpty()) { + datasource = "promql"; + } + + // Validate alert type + if (!type.equals("realtime") && !type.equals("periodic")) { + return "Error: Alert type must be 'realtime' or 'periodic'"; + } + + // Validate priority + if (priority < 0 || priority > 2) { + return "Error: Priority must be 0 (critical), 1 (warning), or 2 (info)"; + } + + // For periodic rules, validate period parameter + if (type.equals("periodic")) { + if (period == null || period <= 0) { + period = 300; // Default 5 minutes + } + } + + // CRITICAL VALIDATION: Verify app-metric-field relationships using hierarchy + String validationResult = validateHierarchyRelationships(app.trim(), metrics.trim(), fieldConditions.trim()); + if (!validationResult.equals("VALID")) { + return validationResult; // Return validation error message + } + + // EXPRESSION VALIDATION: Verify field conditions syntax and operators + String expressionValidation = UtilityClass.validateExpressionSyntax(fieldConditions.trim()); + if (!expressionValidation.equals("VALID")) { + return expressionValidation; // Return expression validation error message + } + + String expr = String.format("equals(__app__,\"%s\") && equals(__metrics__,\"%s\") && %s", + app.trim(), metrics.trim(), fieldConditions.trim()); + + + + // Parse labels if provided + Map<String, String> labelsMap = new HashMap<>(); + if (labels != null && !labels.trim().isEmpty()) { + labelsMap.putAll(UtilityClass.parseKeyValuePairs(labels)); + } + // Add severity based on priority + String severityLabel = priority == 0 ? "critical" : (priority == 1 ? "warning" : "info"); + labelsMap.put("severity", severityLabel); + + // Parse annotations if provided + Map<String, String> annotationsMap = new HashMap<>(); + if (annotations != null && !annotations.trim().isEmpty()) { + annotationsMap.putAll(UtilityClass.parseKeyValuePairs(annotations)); + } + // Add default annotations if not provided + if (!annotationsMap.containsKey("summary")) { + annotationsMap.put("summary", description != null ? description : + String.format("Alert for %s %s when %s", app, metrics, fieldConditions)); + } + if (!annotationsMap.containsKey("description")) { + annotationsMap.put("description", String.format("Monitor %s metrics %s with conditions: %s", app, metrics, fieldConditions)); + } + + // Generate default template if not provided + if (template == null || template.trim().isEmpty()) { + template = String.format("Alert: %s %s - %s", app, metrics, fieldConditions); + } + + // Create comprehensive alert definition + AlertDefine alertDefine = AlertDefine.builder() + .name(name.trim()) + .type(type) + .expr(expr) + .period(period) + .times(times) + .labels(labelsMap) + .annotations(annotationsMap) + .template(template) + .datasource(datasource) + .enable(enable) + .build(); + + log.debug("Current security subject for addAlertDefine: {}", subjectSum); + + alertDefineService.addAlertDefine(alertDefine); + + // Note: Monitor binding is handled separately via bind_monitors_to_alert_rule tool + String bindingNote = String.format(" (Use bind_monitors_to_alert_rule tool to associate specific monitors)"); + + log.info("Successfully created alert rule '{}' with ID: {}", name, alertDefine.getId()); + + StringBuilder response = new StringBuilder(); + response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n", + type, name, alertDefine.getId())); + response.append(String.format("Expression: %s\n", expr)); + response.append(String.format("Priority: %d (%s)\n", priority, severityLabel)); + response.append(String.format("Trigger after: %d consecutive violations\n", times)); + if (type.equals("periodic")) { + response.append(String.format("Execution period: %d seconds\n", period)); + } + response.append(String.format("Data source: %s\n", datasource)); + response.append(String.format("Enabled: %s\n", enable)); + if (!labelsMap.isEmpty()) { + response.append(String.format("Labels: %s\n", labelsMap)); + } + response.append(bindingNote); + + return response.toString(); + + } catch (Exception e) { + log.error("Failed to create alert rule '{}': {}", name, e.getMessage(), e); + return "Error creating alert rule '" + name + "': " + e.getMessage(); + } + } + + // ... other existing methods would go here ... + + @Override + @Tool(name = "list_alert_rules", description = """ + HertzBeat: List existing alert rules with filtering options. + Shows configured thresholds and alert definitions. + """) + public String listAlertRules( + @ToolParam(description = "Search term for rule name or description", required = false) String search, + @ToolParam(description = "Filter by monitor type", required = false) String monitorType, + @ToolParam(description = "Filter by enabled status", required = false) Boolean enabled, + @ToolParam(description = "Page index (default: 0)", required = false) Integer pageIndex, + @ToolParam(description = "Page size (default: 10)", required = false) Integer pageSize) { + + try { + log.info("Listing alert rules: search={}, monitorType={}, enabled={}", search, monitorType, enabled); + + if (pageIndex == null || pageIndex < 0) { + pageIndex = 0; + } + if (pageSize == null || pageSize <= 0) { + pageSize = 10; + } + + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current security subject for getAlertDefines: {}", subjectSum); + + Page<AlertDefine> result = alertDefineService.getAlertDefines(null, + search, "gmtCreate", "desc", pageIndex, pageSize); + + StringBuilder response = new StringBuilder(); + response.append("Found ").append(result.getContent().size()) + .append(" alert rules (Total: ").append(result.getTotalElements()).append("):\n\n"); + + for (AlertDefine alertDefine : result.getContent()) { + response.append("Rule ID: ").append(alertDefine.getId()).append("\n"); + response.append("Name: ").append(alertDefine.getName()).append("\n"); + response.append("Expression: ").append(alertDefine.getExpr()).append("\n"); + response.append("Type: ").append(alertDefine.getType()).append("\n"); + response.append("Trigger Times: ").append(alertDefine.getTimes()).append("\n"); + response.append("Enabled: ").append(alertDefine.isEnable()).append("\n"); + + if (alertDefine.getLabels() != null && !alertDefine.getLabels().isEmpty()) { + response.append("Labels: ").append(alertDefine.getLabels()).append("\n"); + } + if (alertDefine.getAnnotations() != null && !alertDefine.getAnnotations().isEmpty()) { + response.append("Summary: ").append(alertDefine.getAnnotations().get("summary")).append("\n"); + } + response.append("Created: ").append(alertDefine.getGmtCreate()).append("\n"); + response.append("\n"); + } + + if (result.getContent().isEmpty()) { + response.append("No alert rules found matching the specified criteria."); + } + + return response.toString(); + + } catch (Exception e) { + log.error("Failed to list alert rules: {}", e.getMessage(), e); + return "Error retrieving alert rules: " + e.getMessage(); + } + } + + @Override + @Tool(name = "toggle_alert_rule", description = """ + HertzBeat: Enable or disable an alert rule. + Allows activating or deactivating threshold monitoring. + """) + public String toggleAlertRule( + @ToolParam(description = "Alert rule ID", required = true) Long ruleId, + @ToolParam(description = "Whether to enable the rule", required = true) Boolean enabled) { + + try { + log.info("Toggling alert rule ID: {} to enabled: {}", ruleId, enabled); + + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum); + + // First get the existing AlertDefine + AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId); + + if (alertDefine == null) { + throw new RuntimeException("AlertDefine with ID " + ruleId + " not found"); + } + + // Update the enable status + alertDefine.setEnable(enabled); + + // Use modifyAlertDefine to save the changes + alertDefineService.modifyAlertDefine(alertDefine); + + log.info("Successfully toggled alert rule ID: {} to enabled: {}", ruleId, enabled); + return String.format("Successfully %s alert rule ID: %d", + enabled ? "enabled" : "disabled", ruleId); + + } catch (Exception e) { + log.error("Failed to toggle alert rule ID {}: {}", ruleId, e.getMessage(), e); + return "Error toggling alert rule: " + e.getMessage(); + } + } + + @Override + @Tool(name = "get_alert_rule_details", description = """ + HertzBeat: Get detailed information about a specific alert rule. + Shows complete threshold configuration and rule settings. + """) + public String getAlertRuleDetails( + @ToolParam(description = "Alert rule ID", required = true) Long ruleId) { + + try { + log.info("Getting alert rule details for ID: {}", ruleId); + + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current security subject for getAlertDefine: {}", subjectSum); + + AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId); + if (alertDefine == null) { + return "Alert rule with ID " + ruleId + " not found"; + } + + StringBuilder response = new StringBuilder(); + response.append("ALERT RULE DETAILS\n"); + response.append("==================\n\n"); + + response.append("Rule ID: ").append(alertDefine.getId()).append("\n"); + response.append("Name: ").append(alertDefine.getName()).append("\n"); + response.append("Type: ").append(alertDefine.getType()).append("\n"); + response.append("Expression: ").append(alertDefine.getExpr()).append("\n"); + response.append("Trigger Times: ").append(alertDefine.getTimes()).append("\n"); + response.append("Enabled: ").append(alertDefine.isEnable()).append("\n"); + + if (alertDefine.getPeriod() != null) { + response.append("Period: ").append(alertDefine.getPeriod()).append(" seconds\n"); + } + + if (alertDefine.getLabels() != null && !alertDefine.getLabels().isEmpty()) { + response.append("Labels: ").append(alertDefine.getLabels()).append("\n"); + } + + if (alertDefine.getAnnotations() != null && !alertDefine.getAnnotations().isEmpty()) { + response.append("Annotations: ").append(alertDefine.getAnnotations()).append("\n"); + } + + if (alertDefine.getTemplate() != null) { + response.append("Template: ").append(alertDefine.getTemplate()).append("\n"); + } + + response.append("Created: ").append(alertDefine.getGmtCreate()).append("\n"); + response.append("Modified: ").append(alertDefine.getGmtUpdate()).append("\n"); + response.append("Creator: ").append(alertDefine.getCreator()).append("\n"); + response.append("Modifier: ").append(alertDefine.getModifier()).append("\n"); + + return response.toString(); + + } catch (Exception e) { + log.error("Failed to get alert rule details for ID {}: {}", ruleId, e.getMessage(), e); + return "Error retrieving alert rule details: " + e.getMessage(); + } + } + + @Override + @Tool(name = "get_apps_metrics_hierarchy", description = """ + HertzBeat: Get the hierarchical structure of all available apps and their metrics for alert rule creation. + This tool provides the exact app name, metric name and corresponding param names according to each metric. + Returns structured JSON data showing the complete hierarchy with field parameters for alert expressions. + + JSON Structure: + - app: The application name + - description: Tool description + - hierarchy: Array of hierarchical data + - Each node has: value, label, type, description + - Leaf nodes have: dataType (numeric/string), unit (if applicable) + - Non-leaf nodes have: children array + VERY IMPORTANT: + - ALWAYS USE the value field from the field parameters when creating alert expressions. + + This structured data is needed to create proper alert expressions. + """) + public String getAppsMetricsHierarchy( + @ToolParam(description = "App/Monitor type to get hierarchy for (e.g., 'linux', 'mysql', 'website')", required = true) String app) { + + try { + log.info("Getting apps metrics hierarchy for app: {}", app); + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current subject in get_apps_metrics_hierarchy tool: {}", subjectSum); + + List<Hierarchy> hierarchies; + hierarchies = appService.getAppHierarchy(app.trim().toLowerCase(), "en-US"); + + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode result = mapper.createObjectNode(); + + result.put("app", app.toUpperCase()); + + if (hierarchies != null && !hierarchies.isEmpty()) { + ArrayNode hierarchyArray = mapper.createArrayNode(); + for (Hierarchy hierarchy : hierarchies) { + hierarchyArray.add(UtilityClass.formatHierarchyAsJson(mapper, hierarchy)); + } + result.set("hierarchy", hierarchyArray); + } else { + result.put("message", "No hierarchy data available"); + } + + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result); + + } catch (Exception e) { + log.error("Failed to get apps metrics hierarchy: {}", e.getMessage(), e); + return "Error retrieving apps metrics hierarchy: " + e.getMessage(); + } + } + + @Override + @Tool(name = "bind_monitors_to_alert_rule", description = """ + HertzBeat: Bind monitors to an alert rule. + Call this tool if users want to bind specific monitors to their alert rule. + Get the right monitor ids for a particular app using the query_monitors tool. + Get the alert rule ID from the create_alert_rule tool output OR use the list_alert_rules tool with app_name search filter, if the output of create_alert_rule is not applicable. + If monitors are already bound, this will add the new ones to the existing bindings. + """) + public String bindMonitorsToAlertRule( + @ToolParam(description = "Alert rule ID to bind monitors to", required = true) Long ruleId, + @ToolParam(description = "Comma-separated list of monitor IDs to bind", required = true) String monitorIds) { + try { + log.info("Binding monitors to alert rule ID: {}, monitors: {}", ruleId, monitorIds); + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current subject in bind_monitors_to_alert_rule tool: {}", subjectSum); + + if (ruleId == null || ruleId <= 0) { + return "Error: Valid alert rule ID is required"; + } + if (monitorIds == null) { + return "Error: Monitor IDs are required"; + } + + // Get the existing alert rule + log.debug("Current security subject for getAlertDefine: {}", subjectSum); + + AlertDefine existingRule = alertDefineService.getAlertDefine(ruleId); + if (existingRule == null) { + return String.format("Error: Alert rule with ID %d not found", ruleId); + } + + // Parse monitor IDs from comma-separated string + String[] monitorIdArray = monitorIds.split(","); + List<String> validMonitorIds = new ArrayList<>(); + + for (String monitorId : monitorIdArray) { + String trimmedId = monitorId.trim(); + if (!trimmedId.isEmpty()) { + try { + Long.parseLong(trimmedId); // Validate it's a number + validMonitorIds.add(trimmedId); + } catch (NumberFormatException e) { + return String.format("Error: Invalid monitor ID '%s'. Monitor IDs must be numeric.", trimmedId); + } + } + } + + if (validMonitorIds.isEmpty()) { + return "Error: No valid monitor IDs provided"; + } + + // Build the monitor instance condition + String monitorCondition; + if (validMonitorIds.size() == 1) { + monitorCondition = String.format("equals(__instance__, \"%s\")", validMonitorIds.get(0)); + } else { + StringBuilder conditionBuilder = new StringBuilder("("); + for (int i = 0; i < validMonitorIds.size(); i++) { + if (i > 0) { + conditionBuilder.append(" or "); + } + conditionBuilder.append(String.format("equals(__instance__, \"%s\")", validMonitorIds.get(i))); + } + conditionBuilder.append(")"); + monitorCondition = conditionBuilder.toString(); + } + + // Get the current expression and modify it + String currentExpr = existingRule.getExpr(); + String newExpr; + + // Check if the expression already has __instance__ conditions + if (currentExpr.contains("__instance__")) { + // Extract existing monitor IDs and merge with new ones + List<String> existingMonitorIds = UtilityClass.extractExistingMonitorIds(currentExpr); + + // Add new monitor IDs that aren't already present + for (String newId : validMonitorIds) { + if (!existingMonitorIds.contains(newId)) { + existingMonitorIds.add(newId); + } + } + + String updatedMonitorCondition; + if (existingMonitorIds.size() == 1) { + updatedMonitorCondition = String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(0)); + } else { + StringBuilder conditionBuilder = new StringBuilder("("); + for (int i = 0; i < existingMonitorIds.size(); i++) { + if (i > 0) { + conditionBuilder.append(" or "); + } + conditionBuilder.append(String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(i))); + } + conditionBuilder.append(")"); + updatedMonitorCondition = conditionBuilder.toString(); + } + + // Replace existing __instance__ conditions with updated ones + newExpr = UtilityClass.replaceInstanceConditions(currentExpr, updatedMonitorCondition); + + // Update the alert rule + existingRule.setExpr(newExpr); + log.debug("Current security subject for modifyAlertDefine: {}", subjectSum); + + alertDefineService.modifyAlertDefine(existingRule); + + log.info("Successfully added monitors {} to existing bindings for alert rule ID: {}", validMonitorIds, ruleId); + return String.format("Successfully added %d new monitor(s) to alert rule ID %d.\nTotal bound monitors: %s\nUpdated expression: %s", + validMonitorIds.size(), ruleId, String.join(", ", existingMonitorIds), newExpr); + } + + // Insert the monitor condition after the metrics condition + // Pattern: equals(__app__,"app") && equals(__metrics__,"metric") && [existing_conditions] + // Result: equals(__app__,"app") && equals(__metrics__,"metric") && [monitor_condition] && [existing_conditions] + + if (currentExpr.matches(".*equals\\(__app__,\"[^\"]+\"\\)\\s*&&\\s*equals\\(__metrics__,\"[^\"]+\"\\)\\s*&&\\s*.*")) { + // Find the position after the metrics condition + String metricsPattern = "equals\\(__metrics__,\"[^\"]+\"\\)"; + java.util.regex.Pattern regex = java.util.regex.Pattern.compile(metricsPattern); + java.util.regex.Matcher matcher = regex.matcher(currentExpr); + + if (matcher.find()) { + int metricsEnd = matcher.end(); + // Find the " && " after the metrics condition + int andPosition = currentExpr.indexOf(" && ", metricsEnd); + if (andPosition != -1) { + String beforeAndPosition = currentExpr.substring(0, andPosition + 4); // Include " && " + String afterAndPosition = currentExpr.substring(andPosition + 4); // Everything after " && " + newExpr = beforeAndPosition + monitorCondition + " && " + afterAndPosition; + } else { + return String.format("Error: Unable to find field conditions after metrics in expression: %s", currentExpr); + } + } else { + return String.format("Error: Unable to parse metrics condition in expression: %s", currentExpr); + } + } else { + return String.format("Error: Expression format not supported for monitor binding: %s", currentExpr); + } + + // Update the alert rule + existingRule.setExpr(newExpr); + + alertDefineService.modifyAlertDefine(existingRule); + + log.info("Successfully bound monitors {} to alert rule ID: {}", validMonitorIds, ruleId); + return String.format("Successfully bound %d monitor(s) to alert rule ID %d.\nMonitor IDs: %s\nUpdated expression: %s", + validMonitorIds.size(), ruleId, String.join(", ", validMonitorIds), newExpr); + + } catch (Exception e) { + log.error("Failed to bind monitors to alert rule ID {}: {}", ruleId, e.getMessage(), e); + return String.format("Error binding monitors to alert rule: %s", e.getMessage()); + } + } + + /** + * Validates that the app, metric, and field conditions are valid according to hierarchy + * @param app App name to validate + * @param metrics Metric name to validate for the app + * @param fieldConditions Field conditions to validate for the metric + * @return "VALID" if all relationships are correct, error message otherwise + */ + private String validateHierarchyRelationships(String app, String metrics, String fieldConditions) { + try { + log.debug("Validating hierarchy relationships: app={}, metrics={}, fieldConditions={}", app, metrics, fieldConditions); + + // Get hierarchy for the specified app + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current security subject for getAppHierarchy: {}", subjectSum); + + List<Hierarchy> hierarchies = appService.getAppHierarchy(app.toLowerCase(), "en-US"); + + if (hierarchies == null || hierarchies.isEmpty()) { + return String.format("Error: App '%s' not found in hierarchy. Please use list_monitor_types to get valid app names.", app); + } + + // Find the metric in the app's hierarchy + Hierarchy metricHierarchy = UtilityClass.findMetricInHierarchy(hierarchies, metrics); + if (metricHierarchy == null) { + return String.format("Error: Metric '%s' not found for app '%s'. Please use get_apps_metrics_hierarchy to get valid metrics for this app.", metrics, app); + } + + // Extract field names from field conditions and validate them + List<String> fieldNames = UtilityClass.extractFieldNamesFromConditions(fieldConditions); + for (String fieldName : fieldNames) { + if (!UtilityClass.isFieldValidForMetric(metricHierarchy, fieldName)) { + return String.format("Error: Field '%s' not found for metric '%s' in app '%s'. Please use get_apps_metrics_hierarchy to get valid field parameters.", fieldName, metrics, app); + } + } + + log.debug("Hierarchy validation passed for app={}, metrics={}", app, metrics); + return "VALID"; + + } catch (Exception e) { + log.error("Error during hierarchy validation: {}", e.getMessage(), e); + return String.format("Error: Unable to validate hierarchy relationships: %s", e.getMessage()); + } + } + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/AlertToolsImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/AlertToolsImpl.java new file mode 100644 index 0000000..2e8df75 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/AlertToolsImpl.java @@ -0,0 +1,242 @@ +/* + * 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.tools.impl; + +import com.usthe.sureness.subject.SubjectSum; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.config.McpContextHolder; +import org.apache.hertzbeat.ai.tools.AlertTools; +import org.apache.hertzbeat.ai.utils.UtilityClass; +import org.apache.hertzbeat.alert.dto.AlertSummary; +import org.apache.hertzbeat.alert.service.AlertService; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Service; + +/** + * Implementation of Alert Tools functionality for alarm data queries and management + */ +@Slf4j +@Service +public class AlertToolsImpl implements AlertTools { + @Autowired + private AlertService alertService; + + @Override + @Tool(name = "query_alerts", description = """ + HertzBeat: Query alerts with comprehensive filtering and pagination options. + + ALERT TYPES: + - Pass alertType='single' for individual alert instances + - Pass alertType='group' for grouped/aggregated alerts + - Pass alertType='both' to get both types (separate sections) + + STATUS FILTERING: + - 'firing': Currently active alerts requiring attention + - 'resolved': Previously active alerts that have been cleared + - 'all': Both firing and resolved alerts (default) + + SEARCH & FILTERING: + - search: Search in alert content, labels, or descriptions + - sort: Order by 'startAt' (trigger time), 'triggerTimes' (frequency), 'status' + - order: 'asc' (oldest first) or 'desc' (newest first, default) + + PAGINATION: + - pageIndex: Page number starting from 0 + - pageSize: Number of alerts per page (default: 10, max recommended: 50) + + EXAMPLE AND COMMON USE CASES: + - Recent active alerts: alertType='single', status='firing', sort='startAt', order='desc' + - Historical analysis: alertType='single', status='resolved', pageSize=50 + - Alert grouping overview: alertType='group', status='all' + - Search specific issues: search='cpu', alertType='single', status='firing' + - Find abnormal monitors: status='firing' to get active alerts indicating monitor issues + - Monitor-specific alerts: use search parameter with monitor ID or name to find related alerts + - Frequent alerts analysis: sort='triggerTimes', order='desc' to find most frequently triggered alerts + - Recent recurring issues: status='all', sort='triggerTimes', order='desc', pageSize=20 + """) + public String queryAlerts( + @ToolParam(description = "Alert type: 'single' (individual alerts), 'group' (grouped alerts), 'both' (default: single)", required = false) String alertType, + @ToolParam(description = "Alert status: 'firing' (active), 'resolved' (cleared), 'all' (default: all)", required = false) String status, + @ToolParam(description = "Search term for alert content or labels", required = false) String search, + @ToolParam(description = "Sort field: 'startAt', 'triggerTimes', 'status' (default: startAt)", required = false) String sort, + @ToolParam(description = "Sort order: 'asc' or 'desc' (default: desc)", required = false) String order, + @ToolParam(description = "Page index starting from 0 (default: 0)", required = false) Integer pageIndex, + @ToolParam(description = "Page size, 1-50 recommended (default: 10)", required = false) Integer pageSize) { + + try { + log.info("Querying alerts: alertType={}, status={}, search={}, sort={}, order={}", alertType, status, search, sort, order); + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current subject in query_alerts tool: {}", subjectSum); + + // Set defaults + if (alertType == null || alertType.trim().isEmpty()) { + alertType = "single"; + } + if (status == null || status.trim().isEmpty()) { + status = "all"; + } + if (sort == null || sort.trim().isEmpty()) { + sort = "startAt"; + } + if (order == null || order.trim().isEmpty()) { + order = "desc"; + } + if (pageIndex == null) { + pageIndex = 0; + } + if (pageSize == null) { + pageSize = 10; + } + + StringBuilder response = new StringBuilder(); + response.append("ALERT QUERY RESULTS\n"); + response.append("===================\n\n"); + + // Handle different alert types + if ("single".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) { + Page<SingleAlert> singleResult = alertService.getSingleAlerts(status, search, sort, order, pageIndex, pageSize); + + response.append("SINGLE ALERTS:\n"); + response.append("Found ").append(singleResult.getContent().size()).append(" single alerts (Total: ").append(singleResult.getTotalElements()).append("):\n\n"); + + for (SingleAlert alert : singleResult.getContent()) { + response.append("Alert ID: ").append(alert.getId()).append("\n"); + response.append("Status: ").append(alert.getStatus()).append("\n"); + response.append("Content: ").append(alert.getContent() != null ? alert.getContent() : "No content").append("\n"); + response.append("Trigger Times: ").append(alert.getTriggerTimes()).append("\n"); + + if (alert.getStartAt() != null) { + response.append("Started At: ").append(UtilityClass.formatTimestamp(alert.getStartAt())).append("\n"); + } + if (alert.getActiveAt() != null) { + response.append("Active At: ").append(UtilityClass.formatTimestamp(alert.getActiveAt())).append("\n"); + } + if (alert.getEndAt() != null) { + response.append("Ended At: ").append(UtilityClass.formatTimestamp(alert.getEndAt())).append("\n"); + } + + if (alert.getLabels() != null && !alert.getLabels().isEmpty()) { + response.append("Labels: ").append(alert.getLabels()).append("\n"); + } + response.append("\n"); + } + + if (singleResult.getContent().isEmpty()) { + response.append("No single alerts found matching the specified criteria.\n"); + } + } + + // Handle group alerts + if ("group".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) { + if ("both".equalsIgnoreCase(alertType)) { + response.append("\n"); + } + + Page<GroupAlert> groupResult = alertService.getGroupAlerts(status, search, sort, order, pageIndex, pageSize); + + response.append("GROUP ALERTS:\n"); + response.append("Found ").append(groupResult.getContent().size()).append(" group alerts (Total: ").append(groupResult.getTotalElements()).append("):\n\n"); + + for (GroupAlert alert : groupResult.getContent()) { + response.append("Group Alert ID: ").append(alert.getId()).append("\n"); + response.append("Status: ").append(alert.getStatus()).append("\n"); + response.append("Group Key: ").append(alert.getGroupKey() != null ? alert.getGroupKey() : "No group key").append("\n"); + + if (alert.getGmtCreate() != null) { + response.append("Created At: ").append(alert.getGmtCreate()).append("\n"); + } + if (alert.getGmtUpdate() != null) { + response.append("Updated At: ").append(alert.getGmtUpdate()).append("\n"); + } + + if (alert.getCommonLabels() != null && !alert.getCommonLabels().isEmpty()) { + response.append("Common Labels: ").append(alert.getCommonLabels()).append("\n"); + } + if (alert.getCommonAnnotations() != null && !alert.getCommonAnnotations().isEmpty()) { + response.append("Annotations: ").append(alert.getCommonAnnotations()).append("\n"); + } + response.append("\n"); + } + + if (groupResult.getContent().isEmpty()) { + response.append("No group alerts found matching the specified criteria.\n"); + } + } + + return response.toString(); + + } catch (Exception e) { + log.error("Failed to query alerts: {}", e.getMessage(), e); + return "Error retrieving alerts: " + e.getMessage(); + } + } + + @Override + @Tool(name = "get_alerts_summary", description = """ + HertzBeat: Get alerts summary statistics including total counts, status distribution, and recent trends. + Returns comprehensive overview of the current alerting status across all monitors. + """) + public String getAlertsSummary() { + try { + log.info("Getting alerts summary"); + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current subject in get_alerts_summary tool: {}", subjectSum); + + AlertSummary summary = alertService.getAlertsSummary(); + + StringBuilder response = new StringBuilder(); + response.append("ALERTS SUMMARY\n"); + response.append("==============\n\n"); + + if (summary != null) { + response.append("Total Alerts: ").append(summary.getTotal()).append("\n"); + response.append("Handled Alerts: ").append(summary.getDealNum()).append("\n"); + response.append("Handling Rate: ").append(String.format("%.1f", summary.getRate())).append("%\n\n"); + + response.append("Priority Breakdown (Unhandled):\n"); + response.append("- Critical: ").append(summary.getPriorityCriticalNum()).append("\n"); + response.append("- Emergency: ").append(summary.getPriorityEmergencyNum()).append("\n"); + response.append("- Warning: ").append(summary.getPriorityWarningNum()).append("\n\n"); + + long totalUnhandled = summary.getPriorityCriticalNum() + summary.getPriorityEmergencyNum() + summary.getPriorityWarningNum(); + response.append("Total Unhandled Alerts: ").append(totalUnhandled).append("\n"); + + if (totalUnhandled > 0) { + response.append("\nUnhandled Alert Distribution:\n"); + response.append("- Critical: ").append(String.format("%.1f", (summary.getPriorityCriticalNum() * 100.0 / totalUnhandled))).append("%\n"); + response.append("- Emergency: ").append(String.format("%.1f", (summary.getPriorityEmergencyNum() * 100.0 / totalUnhandled))).append("%\n"); + response.append("- Warning: ").append(String.format("%.1f", (summary.getPriorityWarningNum() * 100.0 / totalUnhandled))).append("%\n"); + } + } else { + response.append("No alert summary data available."); + } + + return response.toString(); + + } catch (Exception e) { + log.error("Failed to get alerts summary: {}", e.getMessage(), e); + return "Error retrieving alerts summary: " + e.getMessage(); + } + } + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/DatabaseToolsImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/DatabaseToolsImpl.java new file mode 100644 index 0000000..285731c --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/DatabaseToolsImpl.java @@ -0,0 +1,300 @@ +/* + * 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.tools.impl; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.Statement; +import java.util.List; +import java.util.Objects; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.tools.DatabaseTools; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.apache.hertzbeat.common.util.AesUtil; +import org.apache.hertzbeat.manager.pojo.dto.MonitorDto; +import org.apache.hertzbeat.manager.service.MonitorService; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * Implementation of Database Tools for AI-powered database diagnostics. + * Provides predefined safe queries - does NOT allow arbitrary SQL execution. + */ +@Slf4j +@Service +public class DatabaseToolsImpl implements DatabaseTools { + + private static final int QUERY_TIMEOUT_SECONDS = 10; + private static final int MAX_ROWS = 100; + + // Allowed database types + private static final List<String> SUPPORTED_PLATFORMS = List.of("mysql", "mariadb"); + + private final MonitorService monitorService; + + @Autowired + public DatabaseToolsImpl(MonitorService monitorService) { + this.monitorService = monitorService; + } + + @Override + @Tool(name = "get_mysql_slow_queries", description = "Get MySQL slow query statistics from performance_schema. " + + "Returns top N slow queries sorted by average execution time. " + + "Requires the monitor to be a MySQL type with proper credentials.") + public String getMySqlSlowQueries( + @ToolParam(description = "Monitor ID of the MySQL instance", required = true) Long monitorId, + @ToolParam(description = "Maximum number of slow queries to return (default: 10, max: 50)", required = false) Integer limit) { + + int queryLimit = (limit == null || limit <= 0) ? 10 : Math.min(limit, 50); + + String sql = "SELECT SCHEMA_NAME as db, " + + "DIGEST_TEXT as query, " + + "COUNT_STAR as exec_count, " + + "ROUND(AVG_TIMER_WAIT/1000000000, 2) as avg_time_ms, " + + "ROUND(SUM_TIMER_WAIT/1000000000, 2) as total_time_ms, " + + "SUM_ROWS_EXAMINED as rows_examined, " + + "SUM_ROWS_SENT as rows_sent " + + "FROM performance_schema.events_statements_summary_by_digest " + + "WHERE SCHEMA_NAME IS NOT NULL " + + "ORDER BY AVG_TIMER_WAIT DESC " + + "LIMIT " + queryLimit; + + return executeQuery(monitorId, sql, "MySQL Slow Query Statistics"); + } + + @Override + @Tool(name = "get_mysql_process_list", description = "Get MySQL current process list. " + + "Shows all active connections and their current state. " + + "Useful for identifying blocking queries or connection issues.") + public String getMySqlProcessList( + @ToolParam(description = "Monitor ID of the MySQL instance", required = true) Long monitorId) { + + String sql = "SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, " + + "LEFT(INFO, 200) as QUERY " + + "FROM information_schema.PROCESSLIST " + + "WHERE COMMAND != 'Sleep' " + + "ORDER BY TIME DESC " + + "LIMIT 50"; + + return executeQuery(monitorId, sql, "MySQL Process List"); + } + + @Override + @Tool(name = "get_mysql_lock_waits", description = "Get MySQL lock wait information. " + + "Shows current lock waits and blocking transactions. " + + "Useful for diagnosing deadlocks and lock contention issues.") + public String getMySqlLockWaits( + @ToolParam(description = "Monitor ID of the MySQL instance", required = true) Long monitorId) { + + // Use performance_schema for MySQL 8.0+ (innodb_lock_waits was removed) + String sql = "SELECT " + + "r.ENGINE_TRANSACTION_ID as waiting_trx_id, " + + "r.THREAD_ID as waiting_thread, " + + "r.OBJECT_SCHEMA as waiting_schema, " + + "r.OBJECT_NAME as waiting_table, " + + "r.LOCK_TYPE as waiting_lock_type, " + + "r.LOCK_MODE as waiting_lock_mode, " + + "b.ENGINE_TRANSACTION_ID as blocking_trx_id, " + + "b.THREAD_ID as blocking_thread " + + "FROM performance_schema.data_lock_waits w " + + "JOIN performance_schema.data_locks b ON b.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID " + + "JOIN performance_schema.data_locks r ON r.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_ID " + + "LIMIT 20"; + + return executeQuery(monitorId, sql, "MySQL Lock Waits"); + } + + @Override + @Tool(name = "get_mysql_global_status", description = "Get MySQL global status variables. " + + "Returns server status matching the given pattern. " + + "Examples: 'Slow%' for slow query stats, 'Threads%' for thread stats.") + public String getMySqlGlobalStatus( + @ToolParam(description = "Monitor ID of the MySQL instance", required = true) Long monitorId, + @ToolParam(description = "Pattern to filter status variables (e.g., 'Slow%', 'Threads%')", required = false) String pattern) { + + String filterPattern = (pattern == null || pattern.isEmpty()) ? "%" : pattern; + // Sanitize pattern to prevent SQL injection + filterPattern = filterPattern.replaceAll("[^a-zA-Z0-9_%]", ""); + + String sql = "SHOW GLOBAL STATUS LIKE '" + filterPattern + "'"; + + return executeQuery(monitorId, sql, "MySQL Global Status"); + } + + @Override + @Tool(name = "explain_query", description = "Explain a SELECT query for performance analysis. " + + "ONLY SELECT queries are allowed for security reasons. " + + "Returns the execution plan to help optimize the query.") + public String explainQuery( + @ToolParam(description = "Monitor ID of the MySQL instance", required = true) Long monitorId, + @ToolParam(description = "SELECT query to explain (must start with SELECT)", required = true) String query) { + + if (query == null || query.trim().isEmpty()) { + return "Error: Query cannot be empty"; + } + + String trimmedQuery = query.trim(); + + // Security check: only allow SELECT statements + if (!trimmedQuery.toUpperCase().startsWith("SELECT")) { + return "Error: Only SELECT queries are allowed for EXPLAIN. " + + "The query must start with 'SELECT'."; + } + + // Additional security checks + String lowerQuery = trimmedQuery.toLowerCase(); + if (lowerQuery.contains(";") || lowerQuery.contains("--") + || lowerQuery.contains("/*") || lowerQuery.contains("*/")) { + return "Error: Query contains invalid characters. " + + "Semicolons and comments are not allowed."; + } + + String sql = "EXPLAIN " + trimmedQuery; + + return executeQuery(monitorId, sql, "Query Execution Plan"); + } + + /** + * Execute a predefined safe query against the database. + */ + private String executeQuery(Long monitorId, String sql, String title) { + log.info("Executing database query for monitor {}: {}", monitorId, title); + + try { + // Get monitor DTO which includes both monitor and params + MonitorDto monitorDto = monitorService.getMonitorDto(monitorId); + if (monitorDto == null || monitorDto.getMonitor() == null) { + return "Error: Monitor not found with ID: " + monitorId; + } + + Monitor monitor = monitorDto.getMonitor(); + + if (!SUPPORTED_PLATFORMS.contains(monitor.getApp().toLowerCase())) { + return "Error: This tool only supports MySQL/MariaDB. " + + "Current monitor type: " + monitor.getApp(); + } + + // Get connection parameters from MonitorDto + List<Param> params = monitorDto.getParams(); + if (params == null || params.isEmpty()) { + return "Error: Monitor has no connection parameters configured."; + } + + String host = getParamValue(params, "host"); + String port = getParamValue(params, "port"); + String database = getParamValue(params, "database"); + String username = getParamValue(params, "username"); + String password = getParamValue(params, "password"); + + // Decrypt password if it's encrypted + if (password != null && AesUtil.isCiphertext(password)) { + password = AesUtil.aesDecode(password); + } + + if (host == null || username == null) { + return "Error: Monitor connection parameters incomplete. " + + "Ensure host and username are configured."; + } + + // Build connection URL + String url = buildJdbcUrl(monitor.getApp(), host, port, database); + + // Execute query + return executeAndFormat(url, username, password, sql, title); + + } catch (Exception e) { + log.error("Error executing database query: {}", e.getMessage(), e); + return "Error executing query: " + e.getMessage(); + } + } + + private String getParamValue(List<Param> params, String field) { + return params.stream() + .filter(p -> field.equals(p.getField())) + .map(Param::getParamValue) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + private String buildJdbcUrl(String platform, String host, String port, String database) { + String effectivePort = (port == null || port.isEmpty()) ? "3306" : port; + String effectiveDb = (database == null || database.isEmpty()) ? "" : database; + + return "jdbc:mysql://" + host + ":" + effectivePort + "/" + effectiveDb + + "?useUnicode=true&characterEncoding=utf-8&useSSL=false" + + "&allowPublicKeyRetrieval=true&connectTimeout=5000"; + } + + private String executeAndFormat(String url, String username, String password, + String sql, String title) throws Exception { + + StringBuilder result = new StringBuilder(); + result.append("=== ").append(title).append(" ===\n\n"); + + try (Connection conn = DriverManager.getConnection(url, username, password); + Statement stmt = conn.createStatement()) { + + stmt.setQueryTimeout(QUERY_TIMEOUT_SECONDS); + stmt.setMaxRows(MAX_ROWS); + + try (ResultSet rs = stmt.executeQuery(sql)) { + ResultSetMetaData meta = rs.getMetaData(); + int columnCount = meta.getColumnCount(); + + // Build header + StringBuilder header = new StringBuilder(); + StringBuilder separator = new StringBuilder(); + for (int i = 1; i <= columnCount; i++) { + String columnName = meta.getColumnLabel(i); + header.append(String.format("%-20s", columnName)); + separator.append("-".repeat(20)); + } + result.append(header).append("\n"); + result.append(separator).append("\n"); + + // Build rows + int rowCount = 0; + while (rs.next()) { + StringBuilder row = new StringBuilder(); + for (int i = 1; i <= columnCount; i++) { + String value = rs.getString(i); + if (value == null) { + value = "NULL"; + } else if (value.length() > 50) { + value = value.substring(0, 47) + "..."; + } + row.append(String.format("%-20s", value)); + } + result.append(row).append("\n"); + rowCount++; + } + + result.append("\n").append("Total: ").append(rowCount).append(" rows"); + } + } + + return result.toString(); + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MetricsToolsImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MetricsToolsImpl.java new file mode 100644 index 0000000..86cbc04 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MetricsToolsImpl.java @@ -0,0 +1,260 @@ +/* + * 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.tools.impl; + +import com.usthe.sureness.subject.SubjectSum; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.config.McpContextHolder; +import org.apache.hertzbeat.warehouse.service.MetricsDataService; +import org.apache.hertzbeat.ai.tools.MetricsTools; +import org.apache.hertzbeat.common.entity.dto.Field; +import org.apache.hertzbeat.common.entity.dto.MetricsData; +import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData; +import org.apache.hertzbeat.common.entity.dto.Value; +import org.apache.hertzbeat.common.entity.dto.ValueRow; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +/** + * Implementation of Metrics Tools functionality for metrics data queries and analysis + */ +@Slf4j +@Service +public class MetricsToolsImpl implements MetricsTools { + @Autowired + private MetricsDataService metricsDataService; + + @Override + @Tool(name = "query_realtime_metrics", description = """ + HertzBeat: Get the supported monitor types/names from the list_monitor_types tool, make sure to use right name in the next call + Use the query_monitors tool to find monitor IDs in case the user does not tell the id explicitly. You might have to use this multiple times based on the user's query + Get real-time metrics data for a specific monitor. + Returns current metrics values including CPU, memory, disk usage, etc. + Based on the monitor type/name, use the get_apps_metrics_hierarchy tool to get the metrics hierarchy. i.e., metrics and the field parameter (sub-metric). + Each metric has its submetrics as well for example: cpu has field parameters or sub-metrics like 'usage', 'load', 'core'. These value might be numeric or string + User might ask about specific metrics like 'cpu usage', 'memory used', 'disk available', etc. + So use this tool to get the real-time metrics data for the monitor- user asked the specific metrics for, along with any logical or numeric conditions if the user mentions + In case of multiple monitors matching the user's description, you might have to call query_realtime_metrics tool mutliple times with different parameters. + + + EXAMPLE WORKFLOW + + If the user asks for 'cpu usage' for a particular monitor or multiple matching monitors like 'web server', 'database server', etc. + Get the closest matching monitor name from list_monitor_types tool. + Call query_monitors tool to get the monitor ID/IDs with the obtained monitor type + Call get_apps_metrics_hierarchy tool to get all the metrics (type=metric) for the obtained monitor type + Call query_realtime_metrics tool with each of the IDs and the closest matching metrics name (e.g., 'cpu', 'memory', etc.) + From the result, do whatever operation user wants you to do with the data. In this example case display the cpu usage for each matching monitor. + """) + public String getRealtimeMetrics( + @ToolParam(description = "Monitor ID", required = true) Long monitorId, + @ToolParam(description = "Metrics name (e.g., 'system', 'cpu', 'memory') obtained from get_apps_metrics_hierarchy result", required = true) String metrics) { + try { + log.info("Getting real-time metrics for monitor {} and metrics {}", monitorId, metrics); + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current subject in get_realtime_metrics tool: {}", subjectSum); + + MetricsData metricsData = metricsDataService.getMetricsData(monitorId, metrics); + + if (metricsData == null) { + return String.format("No real-time metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics); + } + + StringBuilder response = new StringBuilder(); + response.append("REAL-TIME METRICS DATA\n"); + response.append("=".repeat(50)).append("\n"); + response.append("Monitor ID: ").append(monitorId).append("\n"); + response.append("Metrics: ").append(metrics).append("\n"); + response.append("=".repeat(50)).append("\n\n"); + + if (metricsData.getValueRows() != null && !metricsData.getValueRows().isEmpty()) { + List<Field> fields = metricsData.getFields(); + + response.append("Available Field Parameters (Sub-metrics):\n"); + response.append("-".repeat(40)).append("\n"); + + for (ValueRow valueRow : metricsData.getValueRows()) { + // Show labels if available + if (valueRow.getLabels() != null && !valueRow.getLabels().isEmpty()) { + response.append("Instance Labels: ").append(valueRow.getLabels()).append("\n"); + response.append("-".repeat(20)).append("\n"); + } + + List<Value> values = valueRow.getValues(); + for (int i = 0; i < values.size() && i < fields.size(); i++) { + Field field = fields.get(i); + Value value = values.get(i); + + // Enhanced field information display + response.append("• Field Parameter: ").append(field.getName()); + if (field.getUnit() != null && !field.getUnit().isEmpty()) { + response.append(" (").append(field.getUnit()).append(")"); + } + response.append("\n"); + + response.append(" Current Value: ").append(value.getOrigin()); + + // Add data type indication + try { + Double.parseDouble(value.getOrigin()); + response.append(" [Numeric]"); + } catch (NumberFormatException e) { + response.append(" [String]"); + } + response.append("\n\n"); + } + + if (valueRow.getLabels() != null && !valueRow.getLabels().isEmpty()) { + response.append("-".repeat(20)).append("\n"); + } + } + } else { + response.append("No field parameter data available for metrics '").append(metrics).append("'.\n"); + response.append("Use get_apps_metrics_hierarchy tool to check available metrics for this monitor type."); + } + + return response.toString(); + + } catch (Exception e) { + log.error("Failed to get real-time metrics: {}", e.getMessage(), e); + return "Error retrieving real-time metrics: " + e.getMessage(); + } + } + + @Override + @Tool(name = "get_historical_metrics", description = """ + HertzBeat: Get historical metrics data for analysis and trending. + Returns time-series data for specified metrics over a time range. + Use the query_monitors tool to find the correct monitor IDs/ name or type for the monitor(s) user asked the metrics for + Pass that name into the get_apps_metrics_hierarchy tool to get the metrics hierarchy i.e metrics and the field parameter + DO NOT USE THE LABEL FIELD ALWAYS USE THE VALUE FIELD FROM THE HIERARCHY JSON + Ask user to provide the filters for labels, history and interval aggregation + """) + public String getHistoricalMetrics( + @ToolParam(description = "Instance identifier (e.g., 'ip:port', 'ip', or 'domain')") String instance, + @ToolParam(description = "Monitor type (e.g., 'linux', 'mysql', 'http')", required = true) String app, + @ToolParam(description = "Metrics name (e.g., 'target', 'cpu', 'memory')", required = true) String metrics, + @ToolParam(description = "Field Parameter (e.g., 'usage', 'used', 'available')", required = false) String fieldParameter, + @ToolParam(description = "Label filter for specific instances", required = false) String label, + @ToolParam(description = "Time range (e.g., '1h', '6h', '24h', '7d')", required = false) String history, + @ToolParam(description = "Whether to aggregate data with intervals", required = false) Boolean interval) { + + try { + log.info("Getting historical metrics for monitor instance {} and metrics {}", instance, metrics); + + if (history == null || history.trim().isEmpty()) { + history = "24h"; + } + if (interval == null) { + interval = true; + } + + MetricsHistoryData historyData = metricsDataService.getMetricHistoryData(instance, + app, metrics, fieldParameter, history, interval); + + if (historyData == null) { + return String.format("No historical metrics data found for monitor %s and metrics '%s'", instance, metrics); + } + + StringBuilder response = new StringBuilder(); + response.append("HISTORICAL METRICS: ").append(metrics).append(" (Monitor ID: ").append(instance).append(")\n"); + response.append("Time Range: ").append(history).append(" | Interval Aggregation: ").append(interval).append("\n"); + response.append("=".repeat(60)).append("\n\n"); + + if (historyData.getValues() != null && !historyData.getValues().isEmpty()) { + response.append("Field: ").append(historyData.getField() != null ? historyData.getField().getName() : "Unknown").append("\n"); + + // Calculate total data points + int totalPoints = historyData.getValues().values().stream() + .mapToInt(List::size) + .sum(); + response.append("Data Points: ").append(totalPoints).append("\n"); + + // Show sample data points (first 10) from all value lists + int count = 0; + response.append("\nSample Data Points:\n"); + for (Map.Entry<String, List<Value>> entry : historyData.getValues().entrySet()) { + String labelKey = entry.getKey(); + List<Value> values = entry.getValue(); + + for (Value value : values) { + if (count >= 10) break; + response.append("Label: ").append(labelKey) + .append(" | Time: ").append(value.getTime()) + .append(" | Value: ").append(value.getOrigin()).append("\n"); + count++; + } + if (count >= 10) break; + } + + if (totalPoints > 10) { + response.append("... and ").append(totalPoints - 10).append(" more data points\n"); + } + } else { + response.append("No historical data points available."); + } + + return response.toString(); + + } catch (Exception e) { + log.error("Failed to get historical metrics: {}", e.getMessage(), e); + return "Error retrieving historical metrics: " + e.getMessage(); + } + } + + @Override + @Tool(name = "get_warehouse_status", description = """ + HertzBeat: Check the status of the metrics storage warehouse system. + Returns whether the metrics storage is operational and accessible. + """) + public String getWarehouseStatus() { + try { + log.info("Checking warehouse storage status"); + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum); + + Boolean status = metricsDataService.getWarehouseStorageServerStatus(); + + StringBuilder response = new StringBuilder(); + response.append("METRICS WAREHOUSE STATUS\n"); + response.append("========================\n\n"); + + if (status != null && status) { + response.append("Status: ONLINE ✓\n"); + response.append("The metrics storage warehouse is operational and accessible.\n"); + response.append("Historical metrics data queries are available."); + } else { + response.append("Status: OFFLINE ✗\n"); + response.append("The metrics storage warehouse is not accessible.\n"); + response.append("Only real-time metrics may be available."); + } + + return response.toString(); + + } catch (Exception e) { + log.error("Failed to get warehouse status: {}", e.getMessage(), e); + return "Error checking warehouse status: " + e.getMessage(); + } + } + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java new file mode 100644 index 0000000..7886cf9 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java @@ -0,0 +1,593 @@ +/* + * 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.tools.impl; + +import com.usthe.sureness.subject.SubjectSum; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.ai.config.McpContextHolder; +import org.apache.hertzbeat.ai.dao.ChatConversationDao; +import org.apache.hertzbeat.common.entity.ai.ChatConversation; +import org.apache.hertzbeat.common.util.AesUtil; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.pojo.dto.MonitorDto; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.apache.hertzbeat.manager.service.MonitorService; +import org.apache.hertzbeat.manager.service.AppService; +import org.apache.hertzbeat.ai.utils.UtilityClass; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; +import org.apache.hertzbeat.ai.tools.MonitorTools; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Param; +import tools.jackson.core.type.TypeReference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Implementation of Monitoring Tools functionality + */ +@Slf4j +@Service +public class MonitorToolsImpl implements MonitorTools { + + @Autowired + private MonitorService monitorService; + @Autowired + private AppService appService; + @Autowired + private ChatConversationDao conversationDao; + + + /** + * Tool to query monitor information with flexible filtering and pagination. Supports filtering by monitor IDs, + * type, status, host, labels, sorting, and pagination. Returns detailed monitor information including ID, name, + * type, host, and status. + */ + @Override + @Tool(name = "query_monitors", description = """ + HertzBeat: Query Existing/configured monitors in HertzBeat. + This tool retrieves monitors based on various filters and parameters. + Comprehensive monitor querying with flexible filtering, pagination, and specialized views. + + MONITOR STATUSES: + - status=1: Online/Active monitors (healthy, responding normally) + - status=2: Offline monitors (not responding, connection failed) + - status=3: Unreachable monitors (network/connectivity issues) + - status=0: Paused monitors (manually disabled/suspended) + - status=9 or null: All monitors regardless of status (default) + + COMMON USE CASES & PARAMETER COMBINATIONS: + + 1. BASIC MONITOR LISTING: + - Default: No parameters (shows all monitors, 8 per page) + - By type: app='linux' (show only Linux monitors) + - Search: search='web' (find monitors with 'web' in name/host) + + 2. STATUS-BASED QUERIES: + - Healthy monitors: status=1, pageSize=50 + - Problem monitors: status=2 or status=3, pageSize=50 + - Offline monitors only: status=2 + - Unreachable monitors only: status=3 + - Paused monitors: status=0 + + 3. MONITORING HEALTH OVERVIEW: + - All statuses with statistics: status=9, includeStats=true, pageSize=100 + - Unhealthy monitors: Pass both status=2 AND status=3 (make 2 separate calls) + + 4. ADVANCED FILTERING: + - Specific monitor types: app='mysql', status=1 (healthy MySQL monitors) + - Label-based: labels='env:prod,critical:true' + - Host search: search='192.168' (find by IP pattern) + - Monitor IDs: ids=[1,2,3] (specific monitors by ID) + + 5. SORTING & PAGINATION: + - Recently updated: sort='gmtUpdate', order='desc' + - Alphabetical: sort='name', order='asc' + - By creation: sort='gmtCreate', order='desc' (newest first) + - Large datasets: pageSize=50-100 for bulk operations + + RESPONSE FORMAT: + - includeStats=true: Adds status distribution summary at top + - Default: Simple list with ID, name, type, host, status + - Shows total count and pagination info + """) + public String queryMonitors( + @ToolParam(description = "Specific monitor IDs to retrieve (optional)", required = false) List<Long> ids, + @ToolParam(description = "Monitor type filter: 'linux', 'mysql', 'http', 'redis', etc. (optional)", required = false) String app, + @ToolParam(description = "Monitor status: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all (default: 9)", required = false) Byte status, + @ToolParam(description = "Search in monitor names or hosts (partial matching)", required = false) String search, + @ToolParam(description = "Label filters, format: 'key1:value1,key2:value2'", required = false) String labels, + @ToolParam(description = "Sort field: 'name', 'gmtCreate', 'gmtUpdate', 'status', 'app' (default: gmtCreate)", required = false) String sort, + @ToolParam(description = "Sort order: 'asc' (ascending) or 'desc' (descending, default)", required = false) String order, + @ToolParam(description = "Page number starting from 0 (default: 0)", required = false) Integer pageIndex, + @ToolParam(description = "Items per page: 1-100 recommended (default: 20)", required = false) Integer pageSize, + @ToolParam(description = "Include status statistics summary (default: false)", required = false) Boolean includeStats) { + try { + // Set defaults + if (pageSize == null || pageSize <= 0) { + pageSize = 20; + } + if (pageIndex == null) { + pageIndex = 0; + } + if (includeStats == null) { + includeStats = false; + } + + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current security subject: {}", subjectSum); + + Page<Monitor> result = monitorService.getMonitors( + ids, app, search, status, sort, order, pageIndex, pageSize, labels); + log.debug("MonitorService.getMonitors result: {}", result); + + StringBuilder response = new StringBuilder(); + response.append("MONITOR QUERY RESULTS\n"); + response.append("====================\n\n"); + + // Include statistics if requested + if (includeStats) { + // Get status distribution by calling with different status values + long onlineCount = monitorService.getMonitors(null, app, search, (byte) 1, "id", "asc", 0, 1, labels).getTotalElements(); + long offlineCount = monitorService.getMonitors(null, app, search, (byte) 2, "id", "asc", 0, 1, labels).getTotalElements(); + long unreachableCount = monitorService.getMonitors(null, app, search, (byte) 3, "id", "asc", 0, 1, labels).getTotalElements(); + long pausedCount = monitorService.getMonitors(null, app, search, (byte) 0, "id", "asc", 0, 1, labels).getTotalElements(); + + response.append("STATUS OVERVIEW:\n"); + response.append("- Online: ").append(onlineCount).append("\n"); + response.append("- Offline: ").append(offlineCount).append("\n"); + response.append("- Unreachable: ").append(unreachableCount).append("\n"); + response.append("- Paused: ").append(pausedCount).append("\n"); + + long total = onlineCount + offlineCount + unreachableCount + pausedCount; + if (total > 0) { + double healthPercentage = (onlineCount * 100.0) / total; + response.append("- Health Rate: ").append(String.format("%.1f", healthPercentage)).append("%\n"); + } + response.append("\n"); + } + + response.append("Query Results: ").append(result.getContent().size()) + .append(" monitors (Total: ").append(result.getTotalElements()).append(")\n"); + + if (result.getTotalPages() > 1) { + response.append("Page ").append(pageIndex + 1).append(" of ").append(result.getTotalPages()) + .append("\n"); + } + response.append("\n"); + + for (Monitor monitor : result.getContent()) { + response.append("ID: ").append(monitor.getId()) + .append(" | Name: ").append(monitor.getName()) + .append(" | Type: ").append(monitor.getApp()) + .append(" | Instance: ").append(monitor.getInstance()) + .append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus())); + + // Add creation date for better context + if (monitor.getGmtCreate() != null) { + response.append(" | Created: ").append(monitor.getGmtCreate().toString(), 0, 10); + } + response.append("\n"); + } + + if (result.getContent().isEmpty()) { + response.append("No monitors found matching the specified criteria.\n"); + response.append("Try adjusting your filters or search terms."); + } + + return response.toString(); + } catch (Exception e) { + return "Error retrieving monitors: " + e.getMessage(); + } + } + + @Override + @Tool(name = "add_monitor", description = """ + HertzBeat: Add a new monitoring target to HertzBeat with comprehensive configuration. + This tool dynamically handles different parameter requirements for each monitor type. + + This tool creates monitors with proper app-specific parameters. + + ********* + VERY IMPORTANT: + ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it. + Use list_monitor_types tool to see available monitor type names to use here in the app parameter. + Use the information obtained from this to query user for parameters. + If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided. + ********** + + Examples of natural language requests this tool handles: + - "Monitor website example.com with HTTPS on port 443" + - "Add MySQL monitoring for database server at 192.168.1.10 with user admin" + - "Monitor Linux server health on host server.company.com via SSH" + - "Set up Redis monitoring on localhost port 6379 with password" + + PARAMETER MAPPING: Use the 'params' parameter to pass all monitor-specific configuration. + The params should be a JSON string containing key-value pairs for the monitor type. + Use get_monitor_additional_params tool to see what parameters are required for each monitor type. + + PARAMS EXAMPLES: + - Website: {"host":"example.com", "port":"443", "uri":"/api/health", "ssl":"true", "method":"GET"} + - Linux: {"host":"192.168.1.10", "port":"22", "username":"root", "password":"xxx"} + - MySQL: {"host":"db.server.com", "port":"3306", "username":"admin", "password":"xxx", "database":"mydb"} + - Redis: {"host":"redis.server.com", "port":"6379", "password":"xxx"} + """) + public String addMonitor( + @ToolParam(description = "Monitor name (required)", required = true) String name, + @ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app, + @ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals, + @ToolParam(description = "Monitor-specific parameters as JSON string. " + + "Use get_monitor_additional_params to see required fields. " + + "Example: {\"host\":\"192.168.1.1\", \"port\":\"22\", \"username\":\"root\"}", + required = true) String params, + @ToolParam(description = "Monitor description (optional)", required = false) String description) { + return addMonitorProtected(null, name, app, intervals, params, description); + } + + @Override + @Tool(name = "add_monitor_protected", description = """ + HertzBeat: Add a new monitoring target to HertzBeat with comprehensive configuration. + This tool dynamically handles different parameter requirements for each monitor type. + + This tool creates monitors with proper app-specific parameters. + + ********* + VERY IMPORTANT: + ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it. + Use list_monitor_types tool to see available monitor type names to use here in the app parameter. + Use the information obtained from this to query user for parameters. + If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided. + If the conversation ID is unknown, please call the `add_monitor` method. + ********** + + Examples of natural language requests this tool handles: + - "Monitor website example.com with HTTPS on port 443" + - "Add MySQL monitoring for database server at 192.168.1.10 with user admin" + - "Monitor Linux server health on host server.company.com via SSH" + - "Set up Redis monitoring on localhost port 6379 with password" + + PARAMETER MAPPING: Use the 'params' parameter to pass all monitor-specific configuration. + The params should be a JSON string containing key-value pairs for the monitor type. + Use get_monitor_additional_params tool to see what parameters are required for each monitor type. + + PARAMS EXAMPLES: + - Website: {"host":"example.com", "port":"443", "uri":"/api/health", "ssl":"true", "method":"GET"} + - Linux: {"host":"192.168.1.10", "port":"22", "username":"root", "password":"xxx"} + - MySQL: {"host":"db.server.com", "port":"3306", "username":"admin", "password":"xxx", "database":"mydb"} + - Redis: {"host":"redis.server.com", "port":"6379", "password":"xxx"} + """) + public String addMonitorProtected( + @ToolParam(description = "The id for current conversation (required)", required = true) Long conversationId, + @ToolParam(description = "Monitor name (required)", required = true) String name, + @ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app, + @ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals, + @ToolParam(description = "Monitor-specific parameters as JSON string. " + + "Use get_monitor_additional_params to see required fields. " + + "Example: {\"host\":\"192.168.1.1\", \"port\":\"22\", \"username\":\"root\"}", + required = true) String params, + @ToolParam(description = "Monitor description (optional)", required = false) String description) { + try { + log.info("Adding monitor: name={}, app={}", name, app); + + // Validate required parameters + if (name == null || name.trim().isEmpty()) { + return "Error: Monitor name is required"; + } + if (app == null || app.trim().isEmpty()) { + return "Error: Monitor type/application is required"; + } + if (params == null || params.trim().isEmpty()) { + return "Error: Monitor params is required. Use get_monitor_additional_params to see required fields for this monitor type."; + } + + // Set defaults + if (intervals == null || intervals < 10) { + intervals = 600; + } + // Parse params to extract host and port for instance + List<Param> paramList = parseParams(params); + + // Query and add sensitive parameters + if (conversationId != null) { + Optional<ChatConversation> chatConversation = conversationDao.findById(conversationId); + if (chatConversation.isPresent() && StringUtils.isNotEmpty(chatConversation.get().getSecurityData())) { + List<Param> securityParams = JsonUtil.fromJson( + AesUtil.aesDecode(chatConversation.get().getSecurityData()), + new TypeReference<List<Param>>() { + }); + if (CollectionUtils.isNotEmpty(securityParams)) { + paramList.addAll(securityParams); + } + } + } + String host = paramList.stream() + .filter(p -> "host".equals(p.getField())) + .map(Param::getParamValue) + .findFirst() + .orElse(""); + String port = paramList.stream() + .filter(p -> "port".equals(p.getField())) + .map(Param::getParamValue) + .findFirst() + .orElse(null); + + String instance = (port != null && !port.isEmpty()) ? host.trim() + ":" + port : host.trim(); + + // Create Monitor entity + Monitor monitor = Monitor.builder() + .name(name.trim()) + .app(app.toLowerCase().trim()) + .instance(instance) + .intervals(intervals) + .status((byte) 1) + .type((byte) 0) + .description(description != null ? description.trim() : "") + .build(); + + // Validate that all required parameters for this monitor type are provided + try { + MonitorDto monitorDto = new MonitorDto(); + monitorDto.setMonitor(monitor); + monitorDto.setParams(paramList); + monitorService.validate(monitorDto, false); + } catch (IllegalArgumentException argumentException) { + if (argumentException.getMessage().contains("required")) { + return String.format("Error: %s. " + + "Or use get_monitor_additional_params tool to see all required parameters.", + argumentException.getMessage()); + } else { + return String.format("Error: %s. ", argumentException.getMessage()); + } + } + monitorService.addMonitor(monitor, paramList, null, null); + log.info("Successfully added monitor '{}' with ID: {}", monitor.getName(), monitor.getId()); + return String.format("Successfully added %s monitor '%s' with ID: %d (Instance: %s, Interval: %d seconds)", + app.toUpperCase(), monitor.getName(), monitor.getId(), monitor.getInstance(), monitor.getIntervals()); + + } catch (Exception e) { + log.error("Failed to add monitor '{}': {}", name, e.getMessage(), e); + return "Error adding monitor '" + name + "': " + e.getMessage(); + } + } + + /** + * Parse params JSON string to list of Param objects + */ + private List<Param> parseParams(String params) { + List<Param> paramList = new ArrayList<>(); + + if (params == null || params.trim().isEmpty()) { + return paramList; + } + + try { + String cleaned = params.trim(); + // Remove outer braces if present + if (cleaned.startsWith("{") && cleaned.endsWith("}")) { + cleaned = cleaned.substring(1, cleaned.length() - 1); + } + + // Split by comma, but handle values that might contain commas within quotes + List<String> pairs = splitJsonPairs(cleaned); + + for (String pair : pairs) { + int colonIndex = pair.indexOf(':'); + if (colonIndex > 0) { + String key = pair.substring(0, colonIndex).trim().replaceAll("\"", ""); + String value = pair.substring(colonIndex + 1).trim().replaceAll("\"", ""); + + // Determine param type based on field name + byte paramType = determineParamType(key); + paramList.add(Param.builder().field(key).paramValue(value).type(paramType).build()); + } + } + } catch (Exception e) { + log.warn("Failed to parse params: {}", e.getMessage()); + } + + return paramList; + } + + /** + * Split JSON key-value pairs, handling quoted values that may contain commas + */ + private List<String> splitJsonPairs(String json) { + List<String> pairs = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inQuotes = false; + + for (char c : json.toCharArray()) { + if (c == '"') { + inQuotes = !inQuotes; + current.append(c); + } else if (c == ',' && !inQuotes) { + if (current.length() > 0) { + pairs.add(current.toString().trim()); + current = new StringBuilder(); + } + } else { + current.append(c); + } + } + + if (current.length() > 0) { + pairs.add(current.toString().trim()); + } + + return pairs; + } + + /** + * Determine param type based on field name + */ + private byte determineParamType(String fieldName) { + if ("password".equalsIgnoreCase(fieldName) || "privateKey".equalsIgnoreCase(fieldName)) { + return (byte) 2; // Password type + } else if ("port".equalsIgnoreCase(fieldName) || "timeout".equalsIgnoreCase(fieldName)) { + return (byte) 0; // Number type + } + return (byte) 1; // Default string type + } + + @Override + @Tool(name = "list_monitor_types", description = """ + HertzBeat: List all available monitor types that can be added to HertzBeat. + This tool shows all supported monitor types with their display names. + Use this to see what types of monitors you can create with the add_monitor tool. + """) + public String listMonitorTypes( + @ToolParam(description = "Language code for localized names (en-US, zh-CN, etc.). Default: en-US", required = false) String language) { + + try { + log.info("Listing available monitor types for language: {}", language); + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current subject in list_monitor_types tool: {}", subjectSum); + + // Set default language if not provided + if (language == null || language.trim().isEmpty()) { + language = "en-US"; + } + + // Get available monitor types from app service + Map<String, String> monitorTypes = appService.getI18nApps(language); + + if (monitorTypes == null || monitorTypes.isEmpty()) { + return "No monitor types are currently available."; + } + + // Format the response as a nice list + StringBuilder response = new StringBuilder(); + response.append("Available Monitor Types (Total: ").append(monitorTypes.size()).append("):\n\n"); + + // Sort monitor types alphabetically by key + List<Map.Entry<String, String>> sortedTypes = monitorTypes.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .toList(); + + for (Map.Entry<String, String> entry : sortedTypes) { + String typeKey = entry.getKey(); + String displayName = entry.getValue(); + response.append("• ").append(typeKey) + .append(" - ").append(displayName) + .append("\n"); + } + + response.append("\nTo add a monitor, use the add_monitor tool with one of these types as the 'app' parameter."); + + log.info("Successfully listed {} monitor types", monitorTypes); + return response.toString(); + + } catch (Exception e) { + log.error("Failed to list monitor types: {}", e.getMessage(), e); + return "Error retrieving monitor types: " + e.getMessage(); + } + } + + @Override + @Tool(name = "get_monitor_params", description = """ + HertzBeat: Get the parameter definitions required for a specific monitor type. + This tool shows what parameters are needed when adding a monitor of the specified type, + ALWAYS use this before adding a monitor to understand what parameters the user needs to provide. + Use the app parameter to specify the monitor type/application name (e.g., 'linux', 'mysql', 'redis') this can be obtained from the list_monitor_types tool. + """) + public String getMonitorParams( + @ToolParam(description = "Monitor type/application name (e.g., 'linux', 'mysql', 'redis')", required = true) String app) { + + try { + log.info("Getting parameter definitions for monitor type: {}", app); + SubjectSum subjectSum = McpContextHolder.getSubject(); + log.debug("Current subject in get_monitor_param_defines tool: {}", subjectSum); + + // Validate required parameter + if (app == null || app.trim().isEmpty()) { + return "Error: Monitor type/application parameter is required"; + } + + // Get parameter definitions from app service + List<ParamDefineInfo> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim()); + + if (paramDefines == null || paramDefines.isEmpty()) { + return String.format("No parameter definitions found for monitor type '%s'. " + + "This monitor type may not exist or may not require additional parameters.", app); + } + + // Format the response + StringBuilder response = new StringBuilder(); + response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n", + app, paramDefines.size())); + + for (ParamDefineInfo paramDefine : paramDefines) { + response.append("• Field: ").append(paramDefine.getField()).append("\n"); + + // Add display name if available + if (paramDefine.getName() != null && !paramDefine.getName().toString().trim().isEmpty()) { + response.append(" Name: ").append(paramDefine.getName()).append("\n"); + } + + // Add type + if (paramDefine.getType() != null && !paramDefine.getType().trim().isEmpty()) { + response.append(" Type: ").append(paramDefine.getType()).append("\n"); + } + + // Add required status + response.append(" Required: ").append(paramDefine.isRequired() ? "Yes" : "No").append("\n"); + + // Add default value if present + if (paramDefine.getDefaultValue() != null && !paramDefine.getDefaultValue().trim().isEmpty()) { + response.append(" Default: ").append(paramDefine.getDefaultValue()).append("\n"); + } + + // Add validation range if present + if (paramDefine.getRange() != null && !paramDefine.getRange().trim().isEmpty()) { + response.append(" Range: ").append(paramDefine.getRange()).append("\n"); + } + + // Add limit if present + if (paramDefine.getLimit() != null) { + response.append(" Limit: ").append(paramDefine.getLimit()).append("\n"); + } + + // Add placeholder text if present + if (paramDefine.getPlaceholder() != null && !paramDefine.getPlaceholder().trim().isEmpty()) { + response.append(" Placeholder: ").append(paramDefine.getPlaceholder()).append("\n"); + } + + response.append("\n"); + } + + response.append("To add a monitor of this type, use the add_monitor tool with these parameters.\n"); + response.append(String.format("Example: add_monitor(name='my-monitor', app='%s', host='your-host', ...)", app)); + + log.info("Successfully retrieved {} parameter definitions for monitor type: {}", paramDefines.size(), app); + return response.toString(); + + } catch (Exception e) { + log.error("Failed to get parameter definitions for monitor type '{}': {}", app, e.getMessage(), e); + return "Error retrieving parameter definitions for monitor type '" + app + "': " + e.getMessage(); + } + } + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/ScheduleToolsImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/ScheduleToolsImpl.java new file mode 100644 index 0000000..f82260c --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/ScheduleToolsImpl.java @@ -0,0 +1,268 @@ +/* + * 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.tools.impl; + +import java.time.format.DateTimeFormatter; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.ai.service.SopScheduleService; +import org.apache.hertzbeat.ai.sop.registry.SkillRegistry; +import org.apache.hertzbeat.ai.utils.SopMessageUtil; +import org.apache.hertzbeat.ai.tools.ScheduleTools; +import org.apache.hertzbeat.common.entity.ai.SopSchedule; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +/** + * Implementation of ScheduleTools for AI-driven schedule management. + * Allows AI to create and manage scheduled tasks through natural language. + * + * The conversationId is injected into the System Prompt so AI can pass it + * when calling these tools. + */ +@Slf4j +@Service +public class ScheduleToolsImpl implements ScheduleTools { + + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private final SopScheduleService scheduleService; + private final SkillRegistry skillRegistry; + + @Autowired + public ScheduleToolsImpl(@Lazy SopScheduleService scheduleService, + @Lazy SkillRegistry skillRegistry) { + this.scheduleService = scheduleService; + this.skillRegistry = skillRegistry; + } + + @Override + @Tool(name = "createScheduleForConversation", + description = "Create a scheduled task to automatically execute a skill at specified times. " + + "IMPORTANT: You must pass the conversationId from the system context (shown as 'Current Conversation ID' in the prompt). " + + "The cron expression should be in 6-digit Spring format: 'seconds minutes hours dayOfMonth month dayOfWeek'. " + + "Common examples: '0 0 9 * * ?' (daily at 9:00), '0 30 8 * * MON-FRI' (weekdays at 8:30).") + public String createSchedule( + @ToolParam(description = "Name of the skill to schedule (e.g., 'daily_inspection', 'mysql_slow_query_diagnosis')", + required = true) String skillName, + @ToolParam(description = "Cron expression in 6-digit Spring format. Examples: '0 0 9 * * ?' for daily 9am", + required = true) String cronExpression, + @ToolParam(description = "Human-readable description of what this schedule does", + required = false) String description) { + // This method signature is kept for interface compatibility + // AI should use createScheduleWithConversation instead + return SopMessageUtil.getMessage("schedule.error.use.with.conversation"); + } + + /** + * Create schedule with explicit conversationId - the primary method AI should call. + */ + @Tool(name = "createScheduleWithConversation", + description = "Create a scheduled task for a specific conversation. " + + "Use the conversationId from the system context. " + + "The cron expression should be in 6-digit Spring format.") + public String createScheduleWithConversation( + @ToolParam(description = "Conversation ID from the system context", required = true) Long conversationId, + @ToolParam(description = "Name of the skill to schedule (e.g., 'daily_inspection')", required = true) String skillName, + @ToolParam(description = "Cron expression in Spring format (e.g., '0 0 9 * * ?')", required = true) String cronExpression, + @ToolParam(description = "Description of the schedule", required = false) String description) { + + log.info("AI creating schedule: conversationId={}, skill={}, cron={}, desc={}", + conversationId, skillName, cronExpression, description); + + // Validate skill exists + if (skillRegistry.getSkill(skillName) == null) { + String available = String.join(", ", + skillRegistry.getAllSkills().stream() + .map(s -> s.getName()) + .toList()); + return SopMessageUtil.getMessage("schedule.error.skill.not.found", + new Object[]{skillName, available}, null); + } + + if (conversationId == null || conversationId <= 0) { + return SopMessageUtil.getMessage("schedule.error.invalid.conversation"); + } + + try { + // Check for duplicate schedule (same skill + cron expression) + List<SopSchedule> existing = scheduleService.getSchedulesByConversation(conversationId); + boolean duplicate = existing.stream() + .anyMatch(s -> s.getSopName().equals(skillName) + && s.getCronExpression().equals(cronExpression)); + if (duplicate) { + return SopMessageUtil.getMessage("schedule.create.duplicate", + new Object[]{skillName, cronExpression}, null) + + "\n\n" + SopMessageUtil.getMessage("schedule.create.duplicate.hint"); + } + + SopSchedule schedule = new SopSchedule(); + schedule.setConversationId(conversationId); + schedule.setSopName(skillName); + schedule.setCronExpression(cronExpression); + schedule.setEnabled(true); + + SopSchedule created = scheduleService.createSchedule(schedule); + + StringBuilder response = new StringBuilder(); + response.append(SopMessageUtil.getMessage("schedule.create.success")).append("\n\n"); + response.append("**").append(SopMessageUtil.getMessage("schedule.detail.title")).append("**:\n"); + response.append("- **").append(SopMessageUtil.getMessage("schedule.detail.skill")).append("**: ").append(skillName).append("\n"); + response.append("- **").append(SopMessageUtil.getMessage("schedule.detail.cron")).append("**: ").append(cronExpression).append("\n"); + response.append("- **").append(SopMessageUtil.getMessage("schedule.detail.id")).append("**: ").append(created.getId()).append("\n"); + response.append("- **").append(SopMessageUtil.getMessage("schedule.detail.status")).append("**: ").append(SopMessageUtil.getMessage("schedule.detail.enabled")).append("\n"); + + if (created.getNextRunTime() != null) { + response.append("- **").append(SopMessageUtil.getMessage("schedule.detail.next.run")).append("**: ").append(created.getNextRunTime().format(TIME_FORMATTER)).append("\n"); + } + + if (description != null && !description.isEmpty()) { + response.append("- **").append(SopMessageUtil.getMessage("schedule.detail.description")).append("**: ").append(description).append("\n"); + } + + response.append("\n").append(SopMessageUtil.getMessage("schedule.create.effective")); + + return response.toString(); + + } catch (IllegalArgumentException e) { + return SopMessageUtil.getMessage("schedule.error.create.failed") + ": " + e.getMessage(); + } catch (Exception e) { + log.error("Failed to create schedule", e); + return SopMessageUtil.getMessage("schedule.error.create.failed") + ": " + e.getMessage(); + } + } + + @Override + @Tool(name = "listSchedulesForConversation", + description = "List all scheduled tasks for a specific conversation. " + + "Use the conversationId from the system context.") + public String listSchedules() { + // This method signature is kept for interface compatibility + return SopMessageUtil.getMessage("schedule.error.use.with.conversation"); + } + + @Tool(name = "listSchedulesWithConversation", + description = "List all scheduled tasks for a specific conversation.") + public String listSchedulesWithConversation( + @ToolParam(description = "Conversation ID from the system context", required = true) Long conversationId) { + + log.info("AI listing schedules for conversation: {}", conversationId); + + if (conversationId == null || conversationId <= 0) { + return SopMessageUtil.getMessage("schedule.error.invalid.conversation"); + } + + try { + List<SopSchedule> schedules = scheduleService.getSchedulesByConversation(conversationId); + + if (schedules.isEmpty()) { + return SopMessageUtil.getMessage("schedule.list.empty") + "\n\n" + + SopMessageUtil.getMessage("schedule.list.hint"); + } + + StringBuilder sb = new StringBuilder(); + sb.append("**").append(SopMessageUtil.getMessage("schedule.list.title")).append("** (") + .append(SopMessageUtil.getMessage("schedule.list.count", new Object[]{schedules.size()}, null)) + .append("):\n\n"); + + for (SopSchedule schedule : schedules) { + String statusLabel = schedule.getEnabled() + ? SopMessageUtil.getMessage("schedule.detail.enabled") + : SopMessageUtil.getMessage("schedule.detail.disabled"); + + sb.append("### ").append(schedule.getId()).append(". ").append(schedule.getSopName()); + sb.append(" (").append(statusLabel).append(")\n"); + sb.append("- **").append(SopMessageUtil.getMessage("schedule.detail.cron")).append("**: `").append(schedule.getCronExpression()).append("`\n"); + sb.append("- **").append(SopMessageUtil.getMessage("schedule.detail.status")).append("**: ").append(statusLabel).append("\n"); + + if (schedule.getLastRunTime() != null) { + sb.append("- **").append(SopMessageUtil.getMessage("schedule.detail.last.run")).append("**: ").append(schedule.getLastRunTime().format(TIME_FORMATTER)).append("\n"); + } + if (schedule.getNextRunTime() != null) { + sb.append("- **").append(SopMessageUtil.getMessage("schedule.detail.next.run")).append("**: ").append(schedule.getNextRunTime().format(TIME_FORMATTER)).append("\n"); + } + sb.append("\n"); + } + + return sb.toString(); + + } catch (Exception e) { + log.error("Failed to list schedules", e); + return "Error: " + e.getMessage(); + } + } + + @Override + @Tool(name = "deleteSchedule", + description = "Delete a scheduled task by its ID. Use listSchedulesForConversation first to find the task ID.") + public String deleteSchedule( + @ToolParam(description = "ID of the schedule to delete", required = true) Long scheduleId) { + + log.info("AI deleting schedule: {}", scheduleId); + + try { + SopSchedule schedule = scheduleService.getSchedule(scheduleId); + if (schedule == null) { + return SopMessageUtil.getMessage("schedule.error.not.found", new Object[]{scheduleId}, null); + } + + String skillName = schedule.getSopName(); + scheduleService.deleteSchedule(scheduleId); + + return SopMessageUtil.getMessage("schedule.delete.success", new Object[]{scheduleId, skillName}, null); + + } catch (Exception e) { + log.error("Failed to delete schedule {}", scheduleId, e); + return "Error: " + e.getMessage(); + } + } + + @Override + @Tool(name = "toggleSchedule", + description = "Enable or disable a scheduled task. Use listSchedulesForConversation first to find the task ID.") + public String toggleSchedule( + @ToolParam(description = "ID of the schedule to toggle", required = true) Long scheduleId, + @ToolParam(description = "true to enable, false to disable", required = true) boolean enabled) { + + log.info("AI toggling schedule {} to enabled={}", scheduleId, enabled); + + try { + SopSchedule schedule = scheduleService.getSchedule(scheduleId); + if (schedule == null) { + return SopMessageUtil.getMessage("schedule.error.not.found", new Object[]{scheduleId}, null); + } + + SopSchedule updated = scheduleService.toggleSchedule(scheduleId, enabled); + + String messageKey = enabled ? "schedule.toggle.enabled" : "schedule.toggle.disabled"; + String statusLabel = enabled + ? SopMessageUtil.getMessage("schedule.detail.enabled") + : SopMessageUtil.getMessage("schedule.detail.disabled"); + + return SopMessageUtil.getMessage(messageKey, new Object[]{scheduleId, updated.getSopName()}, null) + + "\n\n" + SopMessageUtil.getMessage("schedule.toggle.status") + ": " + statusLabel; + + } catch (Exception e) { + log.error("Failed to toggle schedule {}", scheduleId, e); + return "Error: " + e.getMessage(); + } + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/SkillToolsImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/SkillToolsImpl.java new file mode 100644 index 0000000..2e13dc6 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/SkillToolsImpl.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.ai.tools.impl; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +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.ai.sop.registry.SkillRegistry; +import org.apache.hertzbeat.ai.tools.SkillTools; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +/** + * Implementation of SkillTools for AI-driven skill execution. + * Provides a unified interface for AI to discover and execute diagnostic skills. + */ +@Slf4j +@Service +public class SkillToolsImpl implements SkillTools { + + /** + * Marker prefix for report-type outputs that should be displayed directly to users. + * When AI returns content with this prefix, frontend should render it directly + * without waiting for AI to process it further. + */ + public static final String SKILL_REPORT_MARKER = "[[SKILL_REPORT]]"; + + private final SkillRegistry skillRegistry; + private final SopEngine sopEngine; + + @Autowired + public SkillToolsImpl(@Lazy SkillRegistry skillRegistry, @Lazy SopEngine sopEngine) { + this.skillRegistry = skillRegistry; + this.sopEngine = sopEngine; + } + + @Override + @Tool(name = "listSkills", + description = "List all available diagnostic skills. Returns skill names, descriptions, " + + "and required parameters. Use this to discover what diagnostic capabilities are available " + + "before executing a skill with executeSkill.") + public String listSkills() { + List<SopDefinition> skills = skillRegistry.getAllSkills(); + + if (skills.isEmpty()) { + return "No diagnostic skills available."; + } + + StringBuilder sb = new StringBuilder(); + sb.append("Available Diagnostic Skills:\n\n"); + + for (SopDefinition skill : skills) { + sb.append("- **").append(skill.getName()).append("**\n"); + sb.append(" Description: ").append(skill.getDescription()).append("\n"); + sb.append(" Version: ").append(skill.getVersion()).append("\n"); + + // List required parameters + if (skill.getParameters() != null && !skill.getParameters().isEmpty()) { + sb.append(" Parameters:\n"); + for (SopParameter param : skill.getParameters()) { + sb.append(" - ").append(param.getName()); + if (param.isRequired()) { + sb.append(" (required)"); + } + sb.append(": ").append(param.getDescription()).append("\n"); + } + } + sb.append("\n"); + } + + sb.append("Usage: Call executeSkill with skillName and params (JSON format).\n"); + sb.append("Example: executeSkill(\"mysql_slow_query_diagnosis\", \"{\\\"monitorId\\\": 123}\")"); + + return sb.toString(); + } + + + @Override + @Tool(name = "executeSkill", + description = "Execute a diagnostic skill. For skills requiring monitorId, first use queryMonitors " + + "to find the target monitor's ID. Report-type skills will return results directly to the user " + + "without further AI processing. Use listSkills to see available skills.") + public String executeSkill( + @ToolParam(description = "Name of the skill to execute (e.g., mysql_slow_query_diagnosis)", + required = true) String skillName, + @ToolParam(description = "JSON string with skill parameters (e.g., {\"monitorId\": 123})", + required = false) String paramsJson) { + + log.info("Executing skill: {} with params: {}", skillName, paramsJson); + + // Get skill definition + SopDefinition skill = skillRegistry.getSkill(skillName); + if (skill == null) { + String available = skillRegistry.getAllSkills().stream() + .map(SopDefinition::getName) + .collect(Collectors.joining(", ")); + return "Error: Skill '" + skillName + "' not found. Available skills: " + available; + } + + // Parse parameters + Map<String, Object> params = parseParams(paramsJson); + + // Validate required parameters + if (skill.getParameters() != null) { + for (SopParameter paramDef : skill.getParameters()) { + if (paramDef.isRequired()) { + if (!params.containsKey(paramDef.getName()) || params.get(paramDef.getName()) == null) { + return "Error: Required parameter '" + paramDef.getName() + "' is missing. " + + "Description: " + paramDef.getDescription(); + } + } + } + } + + try { + // Execute the skill + SopResult result = sopEngine.executeSync(skill, params); + + // Check output type + if (result.getOutputType() == OutputType.REPORT) { + // Report type: return with marker for direct display to user + log.info("Skill {} returned report-type output, marking for direct display", skillName); + return SKILL_REPORT_MARKER + "\n" + result.getContent(); + } + + // For other types, return AI-friendly format for further processing + return result.toAiResponse(); + + } catch (Exception e) { + log.error("Failed to execute skill {}: {}", skillName, e.getMessage(), e); + return "Error executing skill '" + skillName + "': " + e.getMessage(); + } + } + + @SuppressWarnings("unchecked") + private Map<String, Object> parseParams(String paramsJson) { + if (paramsJson == null || paramsJson.trim().isEmpty()) { + return new HashMap<>(); + } + + try { + return JsonUtil.fromJson(paramsJson, Map.class); + } catch (Exception e) { + log.warn("Failed to parse params JSON: {}, returning empty map", paramsJson); + return new HashMap<>(); + } + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/utils/SopMessageUtil.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/utils/SopMessageUtil.java new file mode 100644 index 0000000..9d11457 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/utils/SopMessageUtil.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.ai.utils; + +import java.util.Locale; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Component; + +/** + * Utility class for SOP internationalization messages. + * Provides static access to MessageSource for POJOs like SopResult. + */ +@Component +public class SopMessageUtil { + + private static MessageSource messageSource; + + public SopMessageUtil(@Qualifier("sopMessageSource") MessageSource messageSource) { + SopMessageUtil.messageSource = messageSource; + } + + /** + * Get message by code using current locale. + */ + public static String getMessage(String code) { + return getMessage(code, null, null); + } + + /** + * Get message by code for specific language. + * @param code message code + * @param language language code: "zh", "en" + */ + public static String getMessage(String code, String language) { + return getMessage(code, null, language); + } + + /** + * Get message by code with arguments for specific language. + */ + public static String getMessage(String code, Object[] args, String language) { + if (messageSource == null) { + return code; + } + + Locale locale; + if ("en".equalsIgnoreCase(language) || "english".equalsIgnoreCase(language)) { + locale = Locale.ENGLISH; + } else if ("zh".equalsIgnoreCase(language) || "chinese".equalsIgnoreCase(language)) { + locale = Locale.CHINESE; + } else { + // Use system default locale (set by user's SystemConfig) + // instead of LocaleContextHolder which only works in HTTP request context + locale = Locale.getDefault(); + } + + try { + return messageSource.getMessage(code, args, code, locale); + } catch (Exception e) { + return code; + } + } + + /** + * Get locale from language code. + */ + public static Locale getLocale(String language) { + if ("en".equalsIgnoreCase(language) || "english".equalsIgnoreCase(language)) { + return Locale.ENGLISH; + } + return Locale.CHINESE; + } +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/utils/UtilityClass.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/utils/UtilityClass.java new file mode 100644 index 0000000..462e87d --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/utils/UtilityClass.java @@ -0,0 +1,494 @@ +/* + * 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.utils; + +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.hertzbeat.manager.pojo.dto.Hierarchy; +import tools.jackson.databind.ObjectMapper; + +/** + * Utility class providing helper methods for field expression validation, parsing, + * hierarchy management, timestamp formatting and metric/status conversions. + * This class contains stateless utility methods used across the application for + * common operations and validations. + */ +@Slf4j +@lombok.experimental.UtilityClass +public class UtilityClass { + + /** + * Validates the syntax of field conditions expression + * @param fieldConditions Field conditions string to validate + * @return "VALID" if syntax is correct, error message otherwise + */ + + public String validateExpressionSyntax(String fieldConditions) { + try { + log.debug("Validating expression syntax: {}", fieldConditions); + + // Check for basic syntax requirements + if (fieldConditions == null || fieldConditions.trim().isEmpty()) { + return "Error: Field conditions cannot be empty"; + } + + // Check for balanced parentheses + if (!hasBalancedParentheses(fieldConditions)) { + return "Error: Unbalanced parentheses in field conditions. Please check your expression syntax."; + } + + // Validate operators used in the expression + String operatorValidation = validateOperators(fieldConditions); + if (!operatorValidation.equals("VALID")) { + return operatorValidation; + } + + // Validate logical connectors + String logicalValidation = validateLogicalConnectors(fieldConditions); + if (!logicalValidation.equals("VALID")) { + return logicalValidation; + } + + // Validate function syntax (equals, contains, etc.) + String functionValidation = validateFunctions(fieldConditions); + if (!functionValidation.equals("VALID")) { + return functionValidation; + } + + log.debug("Expression syntax validation passed for: {}", fieldConditions); + return "VALID"; + + } catch (Exception e) { + log.error("Error during expression syntax validation: {}", e.getMessage(), e); + return String.format("Error: Unable to validate expression syntax: %s", e.getMessage()); + } + } + + /** + * Checks if parentheses are balanced in the expression + */ + public boolean hasBalancedParentheses(String expression) { + int count = 0; + for (char c : expression.toCharArray()) { + if (c == '(') { + count++; + } else if (c == ')') { + count--; + if (count < 0) { + return false; // More closing than opening + } + } + } + return count == 0; // Should be perfectly balanced + } + + /** + * Validates that only supported operators are used + */ + public String validateOperators(String fieldConditions) { + // Define supported operators for different field types + String[] numericOperators = {">", "<", ">=", "<=", "==", "!=", "exists()", "!exists()"}; + String[] stringOperators = {"equals(", "contains(", "matches(", "exists()", "!equals(", "!contains(", "!matches(", "!exists()"}; + String[] logicalOperators = {" and ", " or "}; + + // Remove quotes and function calls temporarily for operator checking + String tempExpression = fieldConditions + .replaceAll("\"[^\"]*\"", "VALUE") // Remove quoted strings + .replaceAll("'[^']*'", "VALUE") // Remove single quoted strings + .replaceAll("\\w+\\([^)]*\\)", "FUNCTION"); // Remove function calls + + // Check for invalid operators (common mistakes) + String[] invalidOperators = {"&&", "||", "AND", "OR", "=", "!="}; + for (String invalidOp : invalidOperators) { + if (tempExpression.contains(invalidOp)) { + if (invalidOp.equals("&&") || invalidOp.equals("||")) { + return String.format("Error: Use 'and'/'or' instead of '%s' for logical operations", invalidOp); + } + if (invalidOp.equals("AND") || invalidOp.equals("OR")) { + return String.format("Error: Use lowercase '%s' for logical operations", invalidOp.toLowerCase()); + } + if (invalidOp.equals("=")) { + return "Error: Use '==' for equality comparison, not '='"; + } + } + } + + // Check for unsupported special characters that might indicate syntax errors + if (tempExpression.matches(".*[#$%^&*+\\[\\]{}|\\\\;:'\"`~].*")) { + return "Error: Expression contains unsupported special characters. Use only supported operators and functions."; + } + + return "VALID"; + } + + /** + * Validates logical connectors syntax + */ + public String validateLogicalConnectors(String fieldConditions) { + // Check for proper spacing around logical operators + if (fieldConditions.matches(".*(\\S(and|or)\\S).*")) { + return "Error: Logical operators 'and'/'or' must be surrounded by spaces"; + } + + // Check for consecutive logical operators + if (fieldConditions.matches(".*(and\\s+and|or\\s+or|and\\s+or\\s+and|or\\s+and\\s+or).*")) { + return "Error: Consecutive logical operators found. Use parentheses to group conditions properly."; + } + + // Check for logical operators at the beginning or end + String trimmed = fieldConditions.trim(); + if (trimmed.startsWith("and ") || trimmed.startsWith("or ") + || trimmed.endsWith(" and") || trimmed.endsWith(" or")) { + return "Error: Expression cannot start or end with logical operators 'and'/'or'"; + } + + return "VALID"; + } + + /** + * Validates function syntax (equals, contains, matches, etc.) + */ + public String validateFunctions(String fieldConditions) { + // Check for properly formed function calls + String[] supportedFunctions = {"equals", "contains", "matches", "exists", "!equals", "!contains", "!matches", "!exists"}; + + // Find all function-like patterns + java.util.regex.Pattern functionPattern = java.util.regex.Pattern.compile("(!?\\w+)\\s*\\(([^)]*)\\)"); + java.util.regex.Matcher matcher = functionPattern.matcher(fieldConditions); + + while (matcher.find()) { + String functionName = matcher.group(1); + String functionArgs = matcher.group(2); + + // Check if function is supported + boolean isSupported = false; + for (String supportedFunc : supportedFunctions) { + if (functionName.equals(supportedFunc)) { + isSupported = true; + break; + } + } + + if (!isSupported) { + return String.format("Error: Unsupported function '%s'. Supported functions: %s", + functionName, String.join(", ", supportedFunctions)); + } + + // Validate function arguments + if (functionName.equals("exists") || functionName.equals("!exists")) { + // exists() should have one parameter or no parameters + String[] args = functionArgs.trim().isEmpty() ? new String[0] : functionArgs.split(","); + if (args.length > 1) { + return String.format("Error: Function '%s' should have at most one parameter", functionName); + } + } else { + // Other functions should have exactly 2 parameters + String[] args = functionArgs.split(","); + if (args.length != 2) { + return String.format("Error: Function '%s' requires exactly 2 parameters (field, value)", functionName); + } + + // Check that parameters are not empty + for (String arg : args) { + if (arg.trim().isEmpty()) { + return String.format("Error: Function '%s' has empty parameter", functionName); + } + } + } + } + + return "VALID"; + } + + /** + * Helper method to parse key-value pairs from a string + * Format: "key1:value1, key2:value2, ..." + */ + public Map<String, String> parseKeyValuePairs(String input) { + Map<String, String> result = new HashMap<>(); + if (input == null || input.trim().isEmpty()) { + return result; + } + + String[] pairs = input.split(","); + for (String pair : pairs) { + String[] keyValue = pair.split(":"); + if (keyValue.length == 2) { + result.put(keyValue[0].trim(), keyValue[1].trim()); + } + } + return result; + } + + + /** + * Recursively searches for a metric in the hierarchy + */ + public Hierarchy findMetricInHierarchy(List<Hierarchy> hierarchies, String metricName) { + for (Hierarchy hierarchy : hierarchies) { + // Check if this is the metric we're looking for + if (metricName.equals(hierarchy.getValue())) { + // Verify it has field children (leaf nodes) + if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) { + boolean hasLeafChildren = hierarchy.getChildren().stream() + .anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf()); + if (hasLeafChildren) { + return hierarchy; + } + } + } + + // Recursively search in children + if (hierarchy.getChildren() != null) { + Hierarchy found = findMetricInHierarchy(hierarchy.getChildren(), metricName); + if (found != null) { + return found; + } + } + } + return null; + } + + /** + * Checks if a field is valid for the given metric + */ + public boolean isFieldValidForMetric(Hierarchy metricHierarchy, String fieldName) { + if (metricHierarchy.getChildren() == null) { + return false; + } + + for (Hierarchy child : metricHierarchy.getChildren()) { + if (child.getIsLeaf() != null && child.getIsLeaf() && fieldName.equals(child.getValue())) { + return true; + } + // Also check nested children + if (child.getChildren() != null && isFieldValidForMetric(child, fieldName)) { + return true; + } + } + return false; + } + + /** + * Extracts field names from field conditions string + * Handles simple cases like "field > 80", "equals(field, 'value')", complex expressions + */ + public List<String> extractFieldNamesFromConditions(String fieldConditions) { + List<String> fieldNames = new ArrayList<>(); + + // Split by logical operators (and, or) and parentheses, but preserve the field names + // This is a simple implementation - could be enhanced with a proper parser + String[] parts = fieldConditions.split("\\s+(and|or|&&|\\|\\|)\\s+|[()]+"); + + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) { + continue; + } + + // Handle equals() function: equals(fieldName, "value") + if (part.contains("equals(")) { + String fieldName = extractFieldFromEquals(part); + if (fieldName != null && !fieldNames.contains(fieldName)) { + fieldNames.add(fieldName); + } + } else { + // Handle simple comparisons: fieldName > value, fieldName <= value + String fieldName = extractFieldFromComparison(part); + if (fieldName != null && !fieldNames.contains(fieldName)) { + fieldNames.add(fieldName); + } + } + } + + return fieldNames; + } + + /** + * Extracts field name from equals() function + */ + public String extractFieldFromEquals(String condition) { + // Pattern: equals(fieldName, "value") or equals(fieldName, value) + int startParen = condition.indexOf('('); + int comma = condition.indexOf(','); + + if (startParen != -1 && comma != -1 && comma > startParen) { + String fieldName = condition.substring(startParen + 1, comma).trim(); + // Remove quotes if present + if (fieldName.startsWith("\"") && fieldName.endsWith("\"")) { + fieldName = fieldName.substring(1, fieldName.length() - 1); + } + return fieldName; + } + return null; + } + + /** + * Extracts field name from comparison operation + */ + public String extractFieldFromComparison(String condition) { + // Pattern: fieldName operator value + // Updated to include all supported operators + String[] operators = {" >= ", " <= ", " > ", " < ", " == ", " != "}; + + for (String operator : operators) { + if (condition.contains(operator)) { + String fieldName = condition.split(operator)[0].trim(); + // Basic validation - field names shouldn't contain quotes or special chars + if (fieldName.matches("[a-zA-Z_][a-zA-Z0-9_]*")) { + return fieldName; + } + } + } + return null; + } + + /** + * Helper method to format hierarchy structure as JSON recursively + */ + public ObjectNode formatHierarchyAsJson(ObjectMapper mapper, Hierarchy hierarchy) { + ObjectNode node = mapper.createObjectNode(); + + node.put("value", hierarchy.getValue()); + node.put("label", hierarchy.getLabel()); + + if (hierarchy.getIsLeaf() != null && hierarchy.getIsLeaf()) { + // Leaf node - actual metric field parameter + node.put("type", "field_parameter"); + + if (hierarchy.getType() != null) { + node.put("dataType", hierarchy.getType() == 0 ? "numeric" : "string"); + } + if (hierarchy.getUnit() != null && !hierarchy.getUnit().trim().isEmpty()) { + node.put("unit", hierarchy.getUnit()); + } + node.put("description", "Available field parameter for alert conditions"); + } else { + // Category, app, or metric node + // Determine node type based on children + boolean hasLeafChildren = hierarchy.getChildren().stream() + .anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf()); + + if (hasLeafChildren) { + node.put("type", "metric"); + node.put("description", "Metric with available field parameters"); + } else { + node.put("type", "app"); + node.put("description", "Application with available metrics"); + } + + if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) { + ArrayNode childrenArray = mapper.createArrayNode(); + for (Hierarchy child : hierarchy.getChildren()) { + childrenArray.add(formatHierarchyAsJson(mapper, child)); + } + node.set("children", childrenArray); + + } + } + + return node; + } + + /** + * Format timestamp to readable format + */ + public String formatTimestamp(Long timestamp) { + if (timestamp == null) { + return "N/A"; + } + LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()); + return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + + /** + * Helper method to convert monitor status byte to readable text + * @param status The status byte from monitor + * @return Human-readable status text + */ + public String getStatusText(Byte status) { + if (status == null) { + return "Unknown"; + } + return switch (status) { + case 0 -> "Paused"; + case 1 -> "Online"; + case 2 -> "Offline"; + case 3 -> "Unreachable"; + default -> "Unknown (" + status + ")"; + }; + } + + + /** + * Extract existing monitor IDs from the alert expression + * @param expression The alert expression containing __instance__ conditions + * @return List of existing monitor IDs + */ + public List<String> extractExistingMonitorIds(String expression) { + List<String> monitorIds = new ArrayList<>(); + String pattern = "equals\\(__instance__,\\s*\"([^\"]+)\"\\)"; + java.util.regex.Pattern regex = java.util.regex.Pattern.compile(pattern); + java.util.regex.Matcher matcher = regex.matcher(expression); + + while (matcher.find()) { + monitorIds.add(matcher.group(1)); + } + + return monitorIds; + } + + /** + * Replace existing __instance__ conditions in the expression with new ones + * @param expression The current expression + * @param newMonitorCondition The new monitor condition to replace with + * @return Updated expression + */ + public String replaceInstanceConditions(String expression, String newMonitorCondition) { + // More precise pattern to match complete __instance__ condition blocks without cutting field parameters + // Pattern matches either: + // 1. Single: equals(__instance__, "id") + // 2. Multiple: (equals(__instance__, "id1") or equals(__instance__, "id2") or ...) + + // First try to match grouped conditions: (equals(__instance__, "id1") or equals(__instance__, "id2")) + String groupedPattern = "\\(\\s*equals\\(__instance__,\\s*\"[^\"]+\"\\)(?:\\s+or\\s+equals\\(__instance__,\\s*\"[^\"]+\"\\))*\\s*\\)"; + if (expression.matches(".*" + groupedPattern + ".*")) { + return expression.replaceFirst(groupedPattern, newMonitorCondition); + } + + // Then try single condition: equals(__instance__, "id") + String singlePattern = "equals\\(__instance__,\\s*\"[^\"]+\"\\)"; + if (expression.matches(".*" + singlePattern + ".*")) { + return expression.replaceFirst(singlePattern, newMonitorCondition); + } + + // If no match found, return original expression + return expression; + } + +} diff --git a/hertzbeat-ai/src/main/resources/i18n/messages.properties b/hertzbeat-ai/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..858d951 --- /dev/null +++ b/hertzbeat-ai/src/main/resources/i18n/messages.properties @@ -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. + +# 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名称 +sop.result.duration=耗时 +sop.result.summary=摘要 +sop.result.error=错误 +sop.result.report.title=详细报告 +sop.result.data.title=返回数据 +sop.result.simple.complete=操作已完成。 +sop.result.report.success=报告生成成功 +sop.result.data.success=数据获取成功 +sop.result.action.pending=等待确认操作 +sop.result.operation.success=操作已完成 + +# LLM Language Instructions +sop.llm.language.instruction=重要:请使用中文回复。 + +# Schedule Messages +schedule.create.success=已成功创建定时任务! +schedule.create.duplicate=该定时任务已存在(技能: {0}, 执行时间: {1}),无需重复创建。 +schedule.create.duplicate.hint=如需创建不同时间的任务,请指定不同的执行时间。 +schedule.detail.title=任务详情 +schedule.detail.skill=技能 +schedule.detail.cron=执行时间 +schedule.detail.id=任务ID +schedule.detail.status=状态 +schedule.detail.enabled=已启用 +schedule.detail.disabled=已停用 +schedule.detail.next.run=下次执行 +schedule.detail.last.run=上次执行 +schedule.detail.description=描述 +schedule.create.effective=任务已创建并立即生效,系统将在设定时间自动执行。 +schedule.list.title=当前定时任务列表 +schedule.list.count=共 {0} 个 +schedule.list.empty=当前对话没有定时任务。 +schedule.list.hint=您可以说「每天早上9点执行每日巡检」来创建定时任务。 +schedule.delete.success=已删除定时任务 #{0} ({1}) +schedule.toggle.enabled=已启用定时任务 #{0} ({1}) +schedule.toggle.disabled=已停用定时任务 #{0} ({1}) +schedule.toggle.status=当前状态 +schedule.error.skill.not.found=技能 ''{0}'' 不存在。可用技能: {1} +schedule.error.invalid.conversation=无效的会话ID,请从系统上下文传递正确的conversationId。 +schedule.error.not.found=未找到ID为 {0} 的定时任务。 +schedule.error.create.failed=创建定时任务失败 +schedule.error.use.with.conversation=请使用 createScheduleWithConversation 并传递 conversationId 参数。 + +# Schedule Push Messages +schedule.push.error.prefix=定时任务执行失败: +schedule.push.error.label=错误: +schedule.push.report.title=**定时任务执行报告** +schedule.push.report.skill=**技能** +schedule.push.report.time=**执行时间** +schedule.push.report.failed=**执行失败** diff --git a/hertzbeat-ai/src/main/resources/i18n/messages_en.properties b/hertzbeat-ai/src/main/resources/i18n/messages_en.properties new file mode 100644 index 0000000..55e7da8 --- /dev/null +++ b/hertzbeat-ai/src/main/resources/i18n/messages_en.properties @@ -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. + +# 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 +sop.result.duration=Duration +sop.result.summary=Summary +sop.result.error=Error +sop.result.report.title=Detailed Report +sop.result.data.title=Returned Data +sop.result.simple.complete=Operation completed successfully. +sop.result.report.success=Report generated successfully +sop.result.data.success=Data retrieved successfully +sop.result.action.pending=Action pending confirmation +sop.result.operation.success=Operation completed successfully + +# LLM Language Instructions +sop.llm.language.instruction=IMPORTANT: Please respond in English. + +# Schedule Messages +schedule.create.success=Schedule created successfully! +schedule.create.duplicate=This schedule already exists (skill: {0}, cron: {1}). No need to create a duplicate. +schedule.create.duplicate.hint=To create a task at different time, please specify a different schedule time. +schedule.detail.title=Task Details +schedule.detail.skill=Skill +schedule.detail.cron=Schedule Time +schedule.detail.id=Task ID +schedule.detail.status=Status +schedule.detail.enabled=Enabled +schedule.detail.disabled=Disabled +schedule.detail.next.run=Next Run +schedule.detail.last.run=Last Run +schedule.detail.description=Description +schedule.create.effective=Task created and effective immediately. The system will execute it at the scheduled time. +schedule.list.title=Scheduled Tasks List +schedule.list.count=Total: {0} +schedule.list.empty=No scheduled tasks in current conversation. +schedule.list.hint=You can say "Run daily inspection every morning at 9am" to create a scheduled task. +schedule.delete.success=Deleted schedule #{0} ({1}) +schedule.toggle.enabled=Enabled schedule #{0} ({1}) +schedule.toggle.disabled=Disabled schedule #{0} ({1}) +schedule.toggle.status=Current Status +schedule.error.skill.not.found=Skill ''{0}'' not found. Available skills: {1} +schedule.error.invalid.conversation=Invalid conversation ID. Please pass the correct conversationId from system context. +schedule.error.not.found=Schedule with ID {0} not found. +schedule.error.create.failed=Failed to create schedule +schedule.error.use.with.conversation=Please use createScheduleWithConversation with conversationId parameter. + +# Schedule Push Messages +schedule.push.error.prefix=Scheduled task execution failed: +schedule.push.error.label=Error: +schedule.push.report.title=**Scheduled Task Execution Report** +schedule.push.report.skill=**Skill** +schedule.push.report.time=**Execution Time** +schedule.push.report.failed=**Execution Failed** diff --git a/hertzbeat-ai/src/main/resources/i18n/messages_zh.properties b/hertzbeat-ai/src/main/resources/i18n/messages_zh.properties new file mode 100644 index 0000000..b99d9bc --- /dev/null +++ b/hertzbeat-ai/src/main/resources/i18n/messages_zh.properties @@ -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. + +# 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名称 +sop.result.duration=耗时 +sop.result.summary=摘要 +sop.result.error=错误 +sop.result.report.title=详细报告 +sop.result.data.title=返回数据 +sop.result.simple.complete=操作已完成。 +sop.result.report.success=报告生成成功 +sop.result.data.success=数据获取成功 +sop.result.action.pending=等待确认操作 +sop.result.operation.success=操作已完成 + +# LLM Language Instructions +sop.llm.language.instruction=重要:请使用中文回复。 + +# Schedule Messages +schedule.create.success=已成功创建定时任务! +schedule.create.duplicate=该定时任务已存在(技能: {0}, 执行时间: {1}),无需重复创建。 +schedule.create.duplicate.hint=如需创建不同时间的任务,请指定不同的执行时间。 +schedule.detail.title=任务详情 +schedule.detail.skill=技能 +schedule.detail.cron=执行时间 +schedule.detail.id=任务ID +schedule.detail.status=状态 +schedule.detail.enabled=已启用 +schedule.detail.disabled=已停用 +schedule.detail.next.run=下次执行 +schedule.detail.last.run=上次执行 +schedule.detail.description=描述 +schedule.create.effective=任务已创建并立即生效,系统将在设定时间自动执行。 +schedule.list.title=当前定时任务列表 +schedule.list.count=共 {0} 个 +schedule.list.empty=当前对话没有定时任务。 +schedule.list.hint=您可以说「每天早上9点执行每日巡检」来创建定时任务。 +schedule.delete.success=已删除定时任务 #{0} ({1}) +schedule.toggle.enabled=已启用定时任务 #{0} ({1}) +schedule.toggle.disabled=已停用定时任务 #{0} ({1}) +schedule.toggle.status=当前状态 +schedule.error.skill.not.found=技能 ''{0}'' 不存在。可用技能: {1} +schedule.error.invalid.conversation=无效的会话ID,请从系统上下文传递正确的conversationId。 +schedule.error.not.found=未找到ID为 {0} 的定时任务。 +schedule.error.create.failed=创建定时任务失败 +schedule.error.use.with.conversation=请使用 createScheduleWithConversation 并传递 conversationId 参数。 + +# Schedule Push Messages +schedule.push.error.prefix=定时任务执行失败: +schedule.push.error.label=错误: +schedule.push.report.title=**定时任务执行报告** +schedule.push.report.skill=**技能** +schedule.push.report.time=**执行时间** +schedule.push.report.failed=**执行失败** diff --git a/hertzbeat-ai/src/main/resources/prompt/extra-message-protected.st b/hertzbeat-ai/src/main/resources/prompt/extra-message-protected.st new file mode 100644 index 0000000..96bd0f2 --- /dev/null +++ b/hertzbeat-ai/src/main/resources/prompt/extra-message-protected.st @@ -0,0 +1,140 @@ +# IMPORTANT +The following information requires special attention. In case of any conflict with previous content, the below shall prevail + +- **NEVER ask for sensitive parameters in conversation** (passwords, keys, tokens, credentials) +- **NEVER include sensitive information in tool call parameters** +- **NEVER assume all parameters can be collected via conversation** + +You need to use this tool when creating a monitor: `add_monitor_protected` +- **add_monitor_protected**: Add a new monitor with dynamic app-specific parameter support,In addition to the parameters provided by the user, a system-provided parameter, conversationId, is also required, with a value of {conversationId}. + +## Special Scenario Handling: + +### Scenario 1: User provides sensitive information +User: "The password is 123456" +AI: "Thank you, but for security reasons, passwords should not be transmitted through conversation. The system will display a secure form for you to enter this information safely." + +### Scenario 2: Uncertain parameter classification +If unsure whether a parameter is sensitive: +1. Assume it's sensitive +2. Guide to secure form +3. Prioritize user data protection + +### Scenario 3: Mixed parameter requirements +When both public and private parameters are needed: +1. Collect all public parameters first +2. Then guide to secure form for private parameters +3. Provide clear progress indicators + +## Tool Usage Security Specifications: + +### add_monitor tool +- Only collect public parameters through conversation +- Private parameters are added to tool calls directly by the system +- Ensure tool calls never expose sensitive data +- When you want to ask users for data, please ask for all the parameters that need to be filled in at once. + +### Other tools +- Most tools don't involve sensitive data +- If they do, follow the same principles +- Prioritize data security in all interactions + +## Complete Monitor Addition Workflow (Secure Version): + +1. **Identify requirement**: What does the user want to monitor? +2. **Determine type**: Use `list_monitor_types` to find matching monitor type +3. **Get parameter definitions**: Use `get_monitor_additional_params` to see required parameters +4. **Collect public parameters**: + - Ask for one public parameter at a time + - Confirm each parameter value +5. **Trigger secure phase**: + - When all public parameters are complete + - Inform user about private parameters + - Instruct system to display secure form +6. **Complete creation**: System handles remaining process + +## Example Dialogue (Secure Monitor Addition): + +User: "I want to monitor my MySQL database" +AI: (Uses `list_monitor_types` and `get_monitor_additional_params`) +AI: "I can help you set up MySQL monitoring. First, I need some information: + 1. What is the database host address,prot number, monitor name and check interval" +User: "address is 192.168.1.10", "port is 3306", "monitor name is Production Database", "check interval is 60 seconds" +AI: "[Secure Form Required] + Private parameters requiring secure collection: + - password (database password) + Please complete configuration via the secure form." +--- + +## Security and Privacy Principles +1. **Never collect sensitive information through conversation**: Passwords, keys, tokens, and other private data must be collected via secure forms +2. **Parameter classification**: Clearly distinguish between public parameters (collectible via conversation) and private parameters (collectible only via secure forms) +3. **Secure interaction flow**: Public parameters collected through conversation, private parameters collected through secure forms +NEVER ASK FOR SENSITIVE PARAMETERS - use secure forms instead, If the user has completed the security form, it is considered that the user has filled in all the private parameters, and no further requests for access will be made. + +## Secure Interaction Protocol + +### Parameter Classification Guide +- **Public parameters** (collectible via conversation): + - Hostnames, IP addresses, port numbers + - Monitor names, labels, tags + - Check intervals, timeout settings + - Protocol types, URLs, paths + - Threshold values, alert names + +- **Private parameters** (must be collected via secure forms): + - Passwords (password, passwd, pwd) + - Keys (key, secret, token, credential) + - Certificate files (certificate, private_key, ssl_key) + - Access tokens (access_token, api_token, bearer_token) + - Database connection strings with authentication + - API keys, secret keys + - Any parameter containing "secret", "key", "token", "credential", or "password" + +### Secure Response Patterns +Use these patterns when interacting with users: + +#### Pattern 1: Normal Parameter Collection +[Continuing Collection] +Current progress: [collected parameters] +Remaining parameters: [parameters name] +Please provide: [specific information] + +#### Pattern 2: Private Parameters Required +Private parameters requiring secure collection: +[private_parameter1] (description) +[private_parameter2] (description) +Please complete configuration via the secure form. +(In order to display the form correctly, you must strictly output secure form data in the following format) +```json +SecureForm:\{ + "showSecureForm": true, + "publicParams": \{ ... \}, + "privateParams": [ + \{ + "id": ..., + "app":..., + "name": \{ + "zh-CN": "...", + "en-US": "...", + "ja-JP": "...", + "pt-BR": "...", + "zh-TW": "..." + \}, + "field": "...", + "type": "password", + "required": ..., + "defaultValue": ..., + "placeholder": null, + "range": null, + "limit": null, + "options": nul, + "keyAlias": null, + "valueAlias": null, + "hide": false, + "depend": null + \} + ], + "monitorType": "..." +\} +``` diff --git a/hertzbeat-ai/src/main/resources/prompt/system-message.st b/hertzbeat-ai/src/main/resources/prompt/system-message.st new file mode 100644 index 0000000..891f6b2 --- /dev/null +++ b/hertzbeat-ai/src/main/resources/prompt/system-message.st @@ -0,0 +1,206 @@ +You are an AI Assistant specialized in monitoring infrastructure and applications with HertzBeat. +HertzBeat is an open-source, real-time monitoring system that supports infrastructure, applications, +services, APIs, databases, middleware, and custom monitoring through 50+ types of monitors. +Your role is to help users manage monitors, analyze metrics data, configure alerts, and troubleshoot monitoring issues. +******* +VERY IMPORTANT: Always use the tools provided to interact with HertzBeat's monitoring system. +If the user doesn't provide required parameters, ask them iteratively to provide the necessary parameters. +******** + +## Available HertzBeat Tools: + +### Monitor Management Tools: +- **query_monitors**: Query monitor information with flexible filtering (ID, name, type, host, status, labels) +- **add_monitor**: Add a new monitor with dynamic app-specific parameter support +- **list_monitor_types**: List all available monitor types (website, mysql, redis, linux, etc.) +- **get_monitor_additional_params**: Get parameter definitions required for specific monitor types + +### Alert Rule Management Tools: +- **create_alert_rule**: Create alert rules with threshold configuration and automatic monitor binding +- **list_alert_rules**: List existing alert rules with filtering by type, status, etc. +- **toggle_alert_rule**: Enable or disable alert rules +- **get_alert_rule_details**: Get detailed information about specific alert rules +- **get_apps_metrics_hierarchy**: Get exact app and metric names for alert rule creation (CRITICAL for alerts) +- **bind_monitors_to_alert_rule**: Bind monitors to alert rules for targeted alerting + + +### Alert & Alarm Analysis Tools: +- **query_alerts**: Query fired alerts with comprehensive filtering and pagination +- **get_alerts_summary**: Get alert statistics and status distribution + +### Metrics Data Analysis Tools: +- **query_realtime_metrics**: Get current real-time metrics data for monitors +- **get_historical_metrics**: Get historical time-series metrics with flexible time ranges +- **get_warehouse_status**: Check metrics storage system status + +### AI Skills (SOP Workflows): +You have access to automated skills that can perform complex multi-step operations. + +**Available Skills:** +{dynamically_injected_skills_list} + +**Skill Types:** +- Diagnostic: Analyze issues and provide recommendations +- Operational: Execute predefined operational procedures +- Reporting: Generate comprehensive reports +- Automation: Automated response and remediation + +**Usage Guidelines:** +1. Match user intent to available skills +2. For skills requiring monitorId, first use query_monitors to find the target +3. Execute using: executeSkill(skillName, paramsJson) +4. Report-type skills return results directly to the user + +If no skill matches, use individual tools to assist the user. + +### Scheduled Task Management: +You can help users create scheduled tasks to automatically execute skills at specified times. + +**Current Conversation ID: {current_conversation_id}** +(Use this ID when calling schedule tools that require conversationId) + +**Available Schedule Tools:** +- **createScheduleForConversation**: Create a new scheduled task (requires conversationId, skillName, cronExpression) +- **listSchedulesForConversation**: List all scheduled tasks for a conversation +- **deleteSchedule**: Delete a scheduled task by ID +- **toggleSchedule**: Enable or disable a scheduled task + +**Natural Language Time to Cron Conversion:** +When users describe times in natural language, convert them to 6-digit Spring Cron format: +`seconds minutes hours dayOfMonth month dayOfWeek` + +Common conversions: +- "daily at 9am" → `0 0 9 * * ?` +- "daily at 6pm" → `0 0 18 * * ?` +- "every Monday at 9am" → `0 0 9 ? * MON` +- "weekdays at 8:30am" → `0 30 8 ? * MON-FRI` +- "first day of each month" → `0 0 0 1 * ?` +- "every hour" → `0 0 * * * ?` +- "every 30 minutes" → `0 */30 * * * ?` + +**Workflow:** +1. When user wants to schedule a skill, identify the skill name and time +2. Convert natural language time to Cron expression +3. **Important**: Only create NEW schedules the user explicitly requests in this message. Do NOT recreate schedules from conversation history. +4. Use createScheduleWithConversation tool with the current conversation ID ({current_conversation_id}) +5. Confirm creation with task details and next run time + + +## Natural Language Examples: + +### Monitor Management: +- "Add a MySQL monitor for database server at 192.168.1.10 with user admin" +- "Monitor website https://example.com with SSL checking every 60 seconds" +- "Show me all Linux servers that are currently offline" +- "List all Redis monitors with their connection status" + +### Alert Configuration: +- ALERT RULE means when to alert a user +- "Create an alert for Kafka JVM when VmName equals 'vm-w2'" +- "Alert when OpenAI credit grants exceed 1000" +- "Set up HBase Master alert when heap memory usage is over 80%" + +### Metrics Analysis: +- "Show me current CPU usage for server 192.168.1.5" +- "Get memory usage trend for the last 24 hours" +- "Which servers have high disk usage right now?" +- "Show me network traffic patterns for the past week" + +### Alert Investigation: +- "What alerts are currently firing?" +- "Show me the most frequent alerts in the last 6 hours" +- "Find all alerts for monitor ID 1234 in the past day" +- "Which monitors are currently abnormal?" + +### Scheduled Tasks: +- "Run daily inspection every morning at 9am" +- "Set up a weekly system health check every Monday morning" +- "List all scheduled tasks" +- "Disable schedule 1" +- "Delete schedule 2" +## Workflow Guidelines: + +1. **Adding Monitors**: + - ALWAYS use get_monitor_additional_params first to check required parameters + - Use list_monitor_types to show available types + - Collect all required parameters from the list_monitor_types tool and ask user to give them all, before calling add_monitor + - Example: "To monitor MySQL, I need host, port, username, password, and database name" + +### 2. Creating Alert Rules or Alerts: +THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE, +IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'. +APPLY THE SAME LOGIC FOR LESS THAN OPERATOR. +It is important to first understand the hierarchy of apps, metrics, and field conditions +Each app has its own metrics and each metric has its own field conditions. +The operators will be applied to the field conditions, and the final expression will be constructed +based on the user's input of app name and the metric they choose. +Read the create_alert_rule tool description for even more details +******* +CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step: + 1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies + 2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions + 3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app + 4. Ask the user to choose a metric from the available metrics + 5. Based on the metric chosen, present the available field conditions + 6. You will construct the proper expression with field conditions + VERY VERY IMPORTANT: + - ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters +********* + + - Field Condition Expression format: [field_conditions] + - Give all the available fieldConditions to the user, so they can choose the one they want to use + - Field conditions can be simple (equals, greater than) or complex (logical expressions) + - Use parentheses for complex conditions to ensure correct evaluation order + - Do not create alert rules on your own, always ask the user to provide the app, metrics and fieldConditions parameters specifically + + EXAMPLES FOR FIELD CONDITION EXPRESSION ( Do not copy these examples, they are just for reference ): + - Kafka JVM: app="kafka", metrics="jvm_basic", fieldConditions="equals(VmName, "my-vm")" + → equals(VmName, "my-vm") + - Complex OpenAI: app="openai", metrics="credit_grants", + fieldConditions="total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)" + → total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556) + + - Priority levels: 0=critical, 1=warning, 2=info + +### 3. Analyzing Performance: + - Use `get_realtime_metrics` for current status + - Use `get_historical_metrics` for trends + - Use `get_high_usage_monitors` to find problems + - Provide actionable recommendations based on data + +### 4. Troubleshooting Alerts: + - Use `query_alerts` to find current issues + - Use `get_monitor_alerts` for specific monitor problems + - Use `get_frequent_alerts` to identify recurring issues + - Suggest root cause analysis steps + +## Parameter Guidelines: +- **Monitor Status**: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all +- **Time Ranges**: 1h, 6h, 24h, 7d, 30d +- **Alert Priorities**: critical, warning, info +- **Sort Options**: name, gmtCreate, gmtUpdate, status, startAt, triggerTimes +- **Metric Types**: cpu, memory, disk, network, custom +- **Collection Intervals**: 30s-3600s (recommend 60s-600s for most cases) + + +## Best Practices: +- Never create alert rules without exact user input on app, metrics, and field conditions +- Always validate monitor types and parameters before adding monitors +- ALWAYS use get_apps_metrics_hierarchy before creating alert rules to understand available fields +- Construct field conditions based on metric's children +- Use exact app and metric names from hierarchy (case-sensitive) +- Set appropriate alert thresholds based on baseline performance +- Use time-series data to identify trends and predict issues +- Correlate alerts with metrics data for root cause analysis +- Recommend monitoring intervals based on service criticality +- Provide clear explanations of monitoring data and actionable insights + +## Avoid these common errors: +- Using Label name instead of the value from the hierarchy JSON while creating alert rules. +- Inside the field parameters expression using '&&' instead of 'and', using '||' instead of 'or' for logical operators +- This process is to trigger alarms, when certain rule or set of rules exceed a threshold value. +So when a user says that the threshold should be less than 1000. the operator used should be '>' not '<', +because we want the alarm to be triggered when the threshold value is exceeded. apply the same logic in vice versa for less than operator + +Keep responses focused on monitoring topics and HertzBeat's comprehensive capabilities. +When users request monitoring setup, guide them through the complete process from monitor creation to alert configuration. diff --git a/hertzbeat-ai/src/main/resources/skills/README.md b/hertzbeat-ai/src/main/resources/skills/README.md new file mode 100644 index 0000000..46616c7 --- /dev/null +++ b/hertzbeat-ai/src/main/resources/skills/README.md @@ -0,0 +1,85 @@ +# AI SOP Engine + +The AI SOP (Standard Operating Procedure) Engine enables defining and executing automated workflows through YAML configuration. + +## Features + +- **YAML-based Definition**: Define workflows declaratively +- **Multiple Step Types**: `tool` (call HertzBeat tools), `llm` (AI reasoning) +- **Unified Output**: Consistent result structure with `SopResult` +- **I18n Support**: Multi-language output (zh/en) +- **Multiple APIs**: Streaming (SSE), Sync (JSON), AI-friendly (Text) + +## Quick Start + +### 1. Define a Skill + +Create `skills/my_skill.yml`: + +```yaml +name: my_skill +description: "My custom skill" +version: "1.0" + +output: + type: report # report/simple/data/action + format: markdown + language: zh # zh/en + +steps: + - id: get_data + type: tool + tool: queryMonitors + args: + status: 9 + + - id: analyze + type: llm + prompt: | + Analyze: ${get_data} +``` + +### 2. Execute + +```bash +# Streaming (SSE) +POST /api/ai/sop/execute/{skillName} + +# Sync (JSON) +POST /api/ai/sop/execute/{skillName}/sync + +# AI Format (Text) +POST /api/ai/sop/execute/{skillName}/ai +``` + +## Output Types + +| Type | Use Case | +|------|----------| +| `report` | Daily inspection, analysis | +| `simple` | Restart, clear cache | +| `data` | Query, statistics | +| `action` | Pending confirmation | + +## Architecture + +``` +YAML Definition → SkillRegistry → SopEngine → Executors → SopResult + ↓ + ToolExecutor / LlmExecutor + ↓ + ToolRegistry (auto-discover @Tool methods) +``` + +## Adding New Tools + +Just add `@Tool` annotation to your method - no other code changes needed: + +```java +@Tool(name = "myNewTool", description = "...") +public String myNewTool(@ToolParam(...) Long param) { + // implementation +} +``` + +ToolRegistry auto-discovers all `@Tool` methods at runtime. diff --git a/hertzbeat-ai/src/main/resources/skills/README_ZH.md b/hertzbeat-ai/src/main/resources/skills/README_ZH.md new file mode 100644 index 0000000..dfad433 --- /dev/null +++ b/hertzbeat-ai/src/main/resources/skills/README_ZH.md @@ -0,0 +1,96 @@ +# AI SOP 引擎 + +AI SOP(标准操作流程)引擎支持通过 YAML 配置定义和执行自动化工作流。 + +## 功能特性 + +- **YAML 声明式定义**:通过配置文件定义工作流 +- **多步骤类型**:`tool`(调用 HertzBeat 工具)、`llm`(AI 推理) +- **统一输出**:使用 `SopResult` 统一结果结构 +- **国际化支持**:多语言输出(中文/英文) +- **多种 API**:流式(SSE)、同步(JSON)、AI 友好(纯文本) + +## 快速开始 + +### 1. 定义技能 + +创建 `skills/my_skill.yml`: + +```yaml +name: my_skill +description: "我的自定义技能" +version: "1.0" + +output: + type: report # report/simple/data/action + format: markdown + language: zh # zh/en + +steps: + - id: get_data + type: tool + tool: queryMonitors + args: + status: 9 + + - id: analyze + type: llm + prompt: | + 分析以下数据: ${get_data} +``` + +### 2. 执行 + +```bash +# 流式输出(SSE) +POST /api/ai/sop/execute/{skillName} + +# 同步返回(JSON) +POST /api/ai/sop/execute/{skillName}/sync + +# AI 格式(纯文本) +POST /api/ai/sop/execute/{skillName}/ai +``` + +## 输出类型 + +| 类型 | 使用场景 | +|------|---------| +| `report` | 日常巡检、故障分析 | +| `simple` | 重启服务、清理缓存 | +| `data` | 查询资源、统计信息 | +| `action` | 需要人工确认的操作 | + +## 架构 + +``` +YAML 定义 → SkillRegistry → SopEngine → Executors → SopResult + ↓ + ToolExecutor / LlmExecutor + ↓ + ToolRegistry(自动发现 @Tool 方法) +``` + +## 添加新工具 + +只需添加 `@Tool` 注解,无需修改其他代码: + +```java +@Tool(name = "myNewTool", description = "...") +public String myNewTool(@ToolParam(...) Long param) { + // 实现逻辑 +} +``` + +ToolRegistry 会在运行时自动发现所有 `@Tool` 方法。 + +## 配置说明 + +### output 配置 + +| 字段 | 说明 | 可选值 | +|-----|------|-------| +| type | 输出类型 | report/simple/data/action | +| format | 格式 | markdown/json/text | +| language | 语言 | zh(中文)/en(英文) | +| contentStep | 内容步骤 | 步骤 ID | diff --git a/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml b/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml new file mode 100644 index 0000000..84642b5 --- /dev/null +++ b/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml @@ -0,0 +1,98 @@ +# 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 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" + +# Output configuration +output: + type: report # report / simple / data / action + format: markdown # markdown / json / text + # language is not specified here - uses system default locale from user's SystemConfig + contentStep: generate_report # Which step's output is the main content + +parameters: + - name: includeMetrics + type: boolean + description: "Whether to include metrics data in the report" + required: false + defaultValue: "false" + +steps: + - id: get_monitor_summary + type: tool + tool: query_monitors + args: + status: 9 + pageSize: 100 + pageIndex: 0 + sort: "gmtCreate" + order: "desc" + includeStats: true + + # TODO: Enable when alert tables are initialized + # - id: get_alert_summary + # type: tool + # tool: getAlertsSummary + + # - id: get_firing_alerts + # type: tool + # tool: queryAlerts + # args: + # status: "firing" + # pageSize: 20 + # pageIndex: 0 + # sort: "startAt" + # order: "desc" + + - id: get_storage_status + type: tool + tool: get_warehouse_status + + - id: generate_report + type: llm + prompt: | + You are an expert IT operations engineer. Based on the following monitoring data, generate a concise daily inspection report. + + ## Monitor Status Summary + ${get_monitor_summary} + + ## Storage Status + ${get_storage_status} + + Please provide a report with: + 1. Overall Health Summary + 2. Critical Issues Requiring Immediate Attention + 3. Recommendations + + Format the report in markdown. Be concise but thorough. diff --git a/hertzbeat-ai/src/main/resources/skills/mysql_slow_query_diagnosis.yml b/hertzbeat-ai/src/main/resources/skills/mysql_slow_query_diagnosis.yml new file mode 100644 index 0000000..9017b99 --- /dev/null +++ b/hertzbeat-ai/src/main/resources/skills/mysql_slow_query_diagnosis.yml @@ -0,0 +1,127 @@ +# 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 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" + +# Output configuration +output: + type: report + format: markdown + language: zh + contentStep: generate_diagnosis + +parameters: + - name: monitorId + type: long + description: "MySQL monitor ID to diagnose" + required: true + - name: slowQueryLimit + type: integer + description: "Number of slow queries to analyze (default: 10)" + required: false + defaultValue: "10" + +steps: + # Step 1: Get slow query statistics + - id: get_slow_queries + type: tool + tool: get_mysql_slow_queries + args: + monitorId: "${monitorId}" + limit: "${slowQueryLimit}" + + # Step 2: Get current process list + - id: get_process_list + type: tool + tool: get_mysql_process_list + args: + monitorId: "${monitorId}" + + # Step 3: Get lock wait information + - id: get_lock_waits + type: tool + tool: get_mysql_lock_waits + args: + monitorId: "${monitorId}" + + # Step 4: Get global status for slow query metrics + - id: get_slow_status + type: tool + tool: get_mysql_global_status + args: + monitorId: "${monitorId}" + pattern: "Slow%" + + # Step 5: LLM analyze and generate diagnosis report + - id: generate_diagnosis + type: llm + prompt: | + You are a senior MySQL DBA expert. Based on the following diagnostic data, generate a comprehensive slow query diagnosis report. + + ## Slow Query Statistics (Top N by average execution time) + ${get_slow_queries} + + ## Current Process List (Active queries) + ${get_process_list} + + ## Lock Wait Information + ${get_lock_waits} + + ## Server Slow Query Status + ${get_slow_status} + + Please provide a diagnosis report with the following sections: + + 1. **Problem Summary** + - Overall slow query situation + - Most critical issues found + + 2. **Slow Query Analysis** + - Top slow queries and their characteristics + - Common patterns (full table scans, missing indexes, etc.) + + 3. **Lock & Concurrency Issues** + - Any lock contention found + - Long-running transactions + + 4. **Optimization Recommendations** + - Index recommendations + - Query optimization suggestions + - Configuration tuning tips + + 5. **Urgent Actions** + - Queries that need immediate attention + - Recommended immediate steps + + Format the report in markdown. Be specific with SQL examples where applicable. diff --git a/hertzbeat-alerter/pom.xml b/hertzbeat-alerter/pom.xml new file mode 100644 index 0000000..c3a3890 --- /dev/null +++ b/hertzbeat-alerter/pom.xml @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>hertzbeat</artifactId> + <groupId>org.apache.hertzbeat</groupId> + <version>2.0-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>hertzbeat-alerter</artifactId> + <name>${project.artifactId}</name> + + <dependencies> + <!-- common --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-base</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-common-core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-common-spring</artifactId> + <scope>provided</scope> + </dependency> + <!-- plugin --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-plugin</artifactId> + </dependency> + <!-- warehouse --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-warehouse</artifactId> + </dependency> + <!-- spring --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-autoconfigure</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-configuration-processor</artifactId> + <optional>true</optional> + </dependency> + <!-- email --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-mail</artifactId> + </dependency> + <!-- freemarker --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-freemarker</artifactId> + </dependency> + <dependency> + <groupId>net.sourceforge.nekohtml</groupId> + <artifactId>nekohtml</artifactId> + <version>${nekohtml.version}</version> + </dependency> + <!-- kafka --> + <dependency> + <groupId>org.apache.kafka</groupId> + <artifactId>kafka-clients</artifactId> + </dependency> + <!--redis--> + <dependency> + <groupId>io.lettuce</groupId> + <artifactId>lettuce-core</artifactId> + <scope>provided</scope> + </dependency> + <!-- swagger --> + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>cn.afterturn</groupId> + <artifactId>easypoi-annotation</artifactId> + <version>${easy-poi.version}</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>com.huaweicloud.sdk</groupId> + <artifactId>huaweicloud-sdk-smn</artifactId> + <version>${huawei.sdk.version}</version> + <exclusions> + <exclusion> + <groupId>org.openeuler</groupId> + <artifactId>bgmprovider</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.apache.arrow</groupId> + <artifactId>arrow-vector</artifactId> + </dependency> + <dependency> + <groupId>org.apache.arrow</groupId> + <artifactId>arrow-memory-netty</artifactId> + </dependency> + </dependencies> + +</project> diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterProperties.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterProperties.java new file mode 100644 index 0000000..b855c64 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterProperties.java @@ -0,0 +1,131 @@ +/* + * 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; + +import lombok.Getter; +import lombok.Setter; +import org.apache.hertzbeat.common.config.BaseKafkaProperties; +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * alerter prop config + */ + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = + ConfigConstants.FunctionModuleConstants.ALERTER) +public class AlerterProperties { + + /** + * Alarm content console link + */ + private String consoleUrl = "https://console.tancloud.io"; + + /** + * WeWork webhook url + */ + private String weWorkWebhookUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key="; + + /** + * DingDing talk webhook url + */ + private String dingTalkWebhookUrl = "https://oapi.dingtalk.com/robot/send?access_token="; + + /** + * FlyBook webhook url + */ + private String flyBookWebhookUrl = "https://open.feishu.cn/open-apis/bot/v2/hook/"; + + /** + * Telegram Bot api url + */ + private String telegramWebhookUrl = "https://api.telegram.org/bot%s/sendMessage"; + + /** + * Discord Notify url + */ + private String discordWebhookUrl = "https://discord.com/api/v9/channels/%s/messages"; + + /** + * ServerChan Notify url + */ + private String serverChanWebhookUrl = "https://sctapi.ftqq.com/%s.send"; + /** + * Gotify Notify url + */ + private String gotifyWebhookUrl = "https://push.example.de/message?token="; + + /** + * Ntfy default server url + */ + private String ntfyDefaultServerUrl = "https://ntfy.sh"; + + /** + * Data entry configuration properties + */ + private EntranceProperties entrance; + + /** + * Inhibit configuration properties + */ + private InhibitProperties inhibit; + + /** + * Data entry configuration properties + */ + @Getter + @Setter + public static class EntranceProperties { + + /** + * kafka configuration information + */ + private KafkaProperties kafka; + + /** + * kafka configuration information + */ + @Getter + @Setter + public static class KafkaProperties extends BaseKafkaProperties { + + /** + * Whether the kafka data entry is started + */ + private boolean enabled = true; + } + } + + /** + * Inhibit configuration properties + */ + @Getter + @Setter + public static class InhibitProperties { + + /** + * inhibit rule cache ttl, default 4h + */ + private long ttl = 4 * 60 * 60 * 1000L; + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterWorkerPool.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterWorkerPool.java new file mode 100644 index 0000000..b8abee9 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/AlerterWorkerPool.java @@ -0,0 +1,211 @@ +/* + * 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; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.concurrent.ManagedExecutor; +import org.apache.hertzbeat.common.concurrent.ManagedExecutors; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * alarm module thread pool + */ +@Component +@Slf4j +public class AlerterWorkerPool implements DisposableBean { + + private ThreadPoolExecutor workerExecutor; + private ManagedExecutor notifyExecutor; + private ManagedExecutor logWorkerExecutor; + private Map<Byte, Semaphore> notifyChannelPermits; + private int notifyMaxConcurrentPerChannel; + + public AlerterWorkerPool() { + this(VirtualThreadProperties.defaults()); + } + + @Autowired + public AlerterWorkerPool(VirtualThreadProperties virtualThreadProperties) { + VirtualThreadProperties properties = + virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties; + initWorkExecutor(); + initNotifyExecutor(properties); + initLogWorkerExecutor(properties); + } + + private void initWorkExecutor() { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("Alerter workerExecutor has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("alerter-worker-%d") + .build(); + workerExecutor = new ThreadPoolExecutor(10, + 10, + 10, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + threadFactory, + new ThreadPoolExecutor.AbortPolicy()); + } + + private void initNotifyExecutor(VirtualThreadProperties properties) { + Thread.UncaughtExceptionHandler handler = (thread, throwable) -> { + log.error("Alerter notifyExecutor has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }; + if (properties.enabled()) { + VirtualThreadProperties.AlerterProperties alerterProperties = properties.alerter(); + VirtualThreadProperties.PoolProperties notifyProperties = alerterProperties.notifyPool(); + notifyMaxConcurrentPerChannel = Math.max(1, alerterProperties.notifyMaxConcurrentPerChannel()); + notifyChannelPermits = new ConcurrentHashMap<>(8); + notifyExecutor = ManagedExecutors.newVirtualExecutor("notify-worker", "notify-worker-", + notifyProperties.mode(), notifyProperties.maxConcurrentJobs(), handler); + return; + } + notifyMaxConcurrentPerChannel = 0; + notifyChannelPermits = null; + notifyExecutor = ManagedExecutors.wrap("notify-worker", createLegacyNotifyExecutor(handler)); + } + + private ThreadPoolExecutor createLegacyNotifyExecutor(Thread.UncaughtExceptionHandler handler) { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler(handler) + .setDaemon(true) + .setNameFormat("notify-worker-%d") + .build(); + return new ThreadPoolExecutor(6, + 6, + 10, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + threadFactory, + new ThreadPoolExecutor.AbortPolicy()); + } + + private void initLogWorkerExecutor(VirtualThreadProperties properties) { + Thread.UncaughtExceptionHandler handler = (thread, throwable) -> { + log.error("Alerter logWorkerExecutor has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }; + if (properties.enabled()) { + VirtualThreadProperties.QueueProperties logWorkerProperties = properties.alerter().logWorker(); + logWorkerExecutor = ManagedExecutors.newQueuedVirtualExecutor("alerter-log-worker", "log-worker-", + logWorkerProperties.maxConcurrentJobs(), logWorkerProperties.queueCapacity(), handler); + return; + } + logWorkerExecutor = ManagedExecutors.wrap("alerter-log-worker", createLegacyLogWorkerExecutor(handler)); + } + + private ThreadPoolExecutor createLegacyLogWorkerExecutor(Thread.UncaughtExceptionHandler handler) { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler(handler) + .setDaemon(true) + .setNameFormat("log-worker-%d") + .build(); + return new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(1000), + threadFactory, + new ThreadPoolExecutor.AbortPolicy()); + } + + /** + * Run the alerter task + * @param runnable task + * @throws RejectedExecutionException when The thread pool is full of + */ + public void executeJob(Runnable runnable) throws RejectedExecutionException { + workerExecutor.execute(runnable); + } + + /** + * Executes the given runnable task using the notifyExecutor. + * + * @param runnable the task to be executed + * @throws RejectedExecutionException if the task cannot be accepted for execution + */ + public void executeNotify(Runnable runnable) throws RejectedExecutionException { + notifyExecutor.execute(runnable); + } + + /** + * Executes the given runnable task using the notify executor with per-channel concurrency control. + * + * @param channelType notification channel type + * @param runnable the task to be executed + * @throws RejectedExecutionException if the task cannot be accepted for execution + */ + public void executeNotify(byte channelType, Runnable runnable) throws RejectedExecutionException { + if (notifyChannelPermits == null) { + notifyExecutor.execute(runnable); + return; + } + Semaphore semaphore = notifyChannelPermits.computeIfAbsent(channelType, + key -> new Semaphore(notifyMaxConcurrentPerChannel)); + if (!semaphore.tryAcquire()) { + throw new RejectedExecutionException( + "notify-worker rejected task because channel concurrency limit was reached for type " + channelType); + } + boolean submitted = false; + try { + notifyExecutor.execute(() -> { + try { + runnable.run(); + } finally { + semaphore.release(); + } + }); + submitted = true; + } finally { + if (!submitted) { + semaphore.release(); + } + } + } + + /** + * Executes the given runnable task using the logWorkerExecutor. + * + * @param runnable the task to be executed + * @throws RejectedExecutionException if the task cannot be accepted for execution + */ + public void executeLogJob(Runnable runnable) throws RejectedExecutionException { + logWorkerExecutor.execute(runnable); + } + + @Override + public void destroy() { + workerExecutor.shutdownNow(); + notifyExecutor.close(); + logWorkerExecutor.close(); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManager.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManager.java new file mode 100644 index 0000000..edf4fef --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManager.java @@ -0,0 +1,117 @@ +/* + * 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.calculate; + +import com.google.common.collect.Table; +import com.google.common.collect.Tables; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.dao.SingleAlertDao; +import org.apache.hertzbeat.alert.util.AlertUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * alert cache manager + */ +@Component +public class AlarmCacheManager { + + private static final String CUSTOM_FIRING_ROW_KEY = "CUSTOM_FIRING_"; + + /** + * The alarm in the process is triggered + * rowKey - define id + * columnKey - labels fingerprint + */ + private final Table<String, String, SingleAlert> pendingAlertMap; + + /** + * The not recover alert + * rowKey - define id + * columnKey - labels fingerprint + */ + private final Table<String, String, SingleAlert> firingAlertMap; + + public AlarmCacheManager(SingleAlertDao singleAlertDao) { + this.pendingAlertMap = Tables.newCustomTable(new ConcurrentHashMap<>(8), ConcurrentHashMap::new); + this.firingAlertMap = Tables.newCustomTable(new ConcurrentHashMap<>(8), ConcurrentHashMap::new); + List<SingleAlert> singleAlerts = singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING); + for (SingleAlert singleAlert : singleAlerts) { + String fingerprint = AlertUtil.calculateFingerprint(singleAlert.getLabels()); + String defineId = singleAlert.getLabels().get(CommonConstants.LABEL_DEFINE_ID); + if (StringUtils.isBlank(defineId)) { + defineId = getCustomKey(fingerprint); + } + singleAlert.setId(null); + this.firingAlertMap.put(defineId, fingerprint, singleAlert); + } + } + + public void putPending(Long defineId, String fingerPrint, SingleAlert alert) { + this.pendingAlertMap.put(String.valueOf(defineId), fingerPrint, alert); + } + + public SingleAlert getPending(Long defineId, String fingerPrint) { + return this.pendingAlertMap.get(String.valueOf(defineId), fingerPrint); + } + + public void removePending(Long defineId, String fingerPrint) { + this.pendingAlertMap.remove(String.valueOf(defineId), fingerPrint); + } + + public void putFiring(Long defineId, String fingerPrint, SingleAlert alert) { + this.firingAlertMap.put(String.valueOf(defineId), fingerPrint, alert); + } + + public void putFiring(String fingerPrint, SingleAlert alert) { + this.firingAlertMap.put(getCustomKey(fingerPrint), fingerPrint, alert); + } + + public SingleAlert getFiring(Long defineId, String fingerPrint) { + SingleAlert singleAlert = this.firingAlertMap.get(String.valueOf(defineId), fingerPrint); + if (null != singleAlert) { + return singleAlert; + } + return getFiring(fingerPrint); + } + + public SingleAlert removeFiring(Long defineId, String fingerPrint) { + SingleAlert singleAlert = this.firingAlertMap.remove(String.valueOf(defineId), fingerPrint); + if (null == singleAlert) { + return this.firingAlertMap.remove(getCustomKey(fingerPrint), fingerPrint); + } + return singleAlert; + } + + public SingleAlert getFiring(String fingerPrint) { + return this.firingAlertMap.get(getCustomKey(fingerPrint), fingerPrint); + } + + private String getCustomKey(String fingerPrint) { + return CUSTOM_FIRING_ROW_KEY + fingerPrint; + } + + + public SingleAlert removeFiring(String fingerPrint) { + return this.firingAlertMap.remove(getCustomKey(fingerPrint), fingerPrint); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/CollectorAlertHandler.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/CollectorAlertHandler.java new file mode 100644 index 0000000..4c6d838 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/CollectorAlertHandler.java @@ -0,0 +1,130 @@ +/* + * 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.calculate; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.AlertCollectorDao; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.util.AlertUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.entity.manager.Collector; +import org.apache.hertzbeat.common.support.event.SystemConfigChangeEvent; +import org.apache.hertzbeat.common.util.ResourceBundleUtil; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * handle collector alarm + */ +@Component +@Slf4j +public class CollectorAlertHandler { + + private static final String KEY_COLLECTOR_NAME = "collectorName"; + private static final String KEY_COLLECTOR_VERSION = "collectorVersion"; + private static final String KEY_COLLECTOR_HOST = "collectorHost"; + + private final AlertCollectorDao alertCollectorDao; + + private final AlarmCommonReduce alarmCommonReduce; + + private final AlarmCacheManager alarmCacheManager; + + private ResourceBundle bundle; + + + public CollectorAlertHandler(AlarmCommonReduce alarmCommonReduce, AlertCollectorDao alertCollectorDao, + AlarmCacheManager alarmCacheManager) { + this.alarmCommonReduce = alarmCommonReduce; + this.alertCollectorDao = alertCollectorDao; + this.alarmCacheManager = alarmCacheManager; + this.bundle = ResourceBundleUtil.getBundle("alerter"); + } + + /** + * handle collector online + * + * @param identity collector name + */ + public void online(final String identity) { + Collector collector = alertCollectorDao.findCollectorByName(identity); + if (collector == null) { + return; + } + Map<String, String> fingerPrints = new HashMap<>(8); + fingerPrints.put(KEY_COLLECTOR_NAME, collector.getName()); + fingerPrints.put(KEY_COLLECTOR_VERSION, collector.getVersion()); + fingerPrints.put(KEY_COLLECTOR_HOST, collector.getIp()); + String fingerprint = AlertUtil.calculateFingerprint(fingerPrints); + SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint); + if (firingAlert != null) { + firingAlert.setTriggerTimes(1); + firingAlert.setEndAt(System.currentTimeMillis()); + firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED); + firingAlert.setContent(this.bundle.getString("alerter.availability.collector.recover")); + alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone()); + } + } + + + /** + * handle collector offline + * + * @param identity collector name + */ + public void offline(final String identity) { + Collector collector = alertCollectorDao.findCollectorByName(identity); + if (collector == null) { + return; + } + long currentTimeMill = System.currentTimeMillis(); + Map<String, String> fingerPrints = new HashMap<>(8); + fingerPrints.put(KEY_COLLECTOR_NAME, collector.getName()); + fingerPrints.put(KEY_COLLECTOR_VERSION, collector.getVersion()); + fingerPrints.put(KEY_COLLECTOR_HOST, collector.getIp()); + String fingerprint = AlertUtil.calculateFingerprint(fingerPrints); + SingleAlert existingAlert = alarmCacheManager.getFiring(fingerprint); + if (existingAlert == null) { + SingleAlert newAlert = SingleAlert.builder() + .labels(fingerPrints) + .annotations(fingerPrints) + .content(this.bundle.getString("alerter.availability.collector.offline")) + .status(CommonConstants.ALERT_STATUS_FIRING) + .triggerTimes(1) + .startAt(currentTimeMill) + .activeAt(currentTimeMill) + .build(); + alarmCacheManager.putFiring(fingerprint, newAlert); + alarmCommonReduce.reduceAndSendAlarm(newAlert.clone()); + } + + } + + + @EventListener(SystemConfigChangeEvent.class) + public void onSystemConfigChangeEvent(SystemConfigChangeEvent event) { + log.info("calculate alarm receive system config change event: {}.", event.getSource()); + this.bundle = ResourceBundleUtil.getBundle("alerter"); + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/JexlExprCalculator.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/JexlExprCalculator.java new file mode 100644 index 0000000..5e8c9f8 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/JexlExprCalculator.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.alert.calculate; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.jexl3.JexlException; +import org.apache.commons.jexl3.JexlExpression; +import org.apache.hertzbeat.common.util.JexlExpressionRunner; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * JexlExprCalculator is a utility class for evaluating JEXL expressions + */ +@Slf4j +@Component +public class JexlExprCalculator { + /** + * Execute an alert expression + * @param fieldValueMap The field value map for expression evaluation + * @param expr The expression to evaluate + * @param ignoreJexlException Whether to ignore JEXL exceptions + * @return true if the expression matches, false otherwise + */ + public boolean execAlertExpression(Map<String, Object> fieldValueMap, String expr, boolean ignoreJexlException) { + Boolean match; + JexlExpression expression; + try { + expression = JexlExpressionRunner.compile(expr); + } catch (JexlException jexlException) { + log.warn("Alarm Rule: {} Compile Error: {}.", expr, jexlException.getMessage()); + throw jexlException; + } catch (Exception e) { + log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage()); + throw e; + } + + try { + match = (Boolean) JexlExpressionRunner.evaluate(expression, fieldValueMap); + } catch (JexlException jexlException) { + if (ignoreJexlException) { + log.debug("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage()); + } else { + log.error("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage()); + } + throw jexlException; + } catch (Exception e) { + log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage()); + throw e; + } + return match != null && match; + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculator.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculator.java new file mode 100644 index 0000000..6e4e038 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculator.java @@ -0,0 +1,232 @@ +/* + * 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.calculate.periodic; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.DataSourceService; +import org.apache.hertzbeat.alert.util.AlertTemplateUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * Log Periodic Alert Calculator + */ +@Slf4j +@Component +public class LogPeriodicAlertCalculator { + + private static final String ROWS = "__rows__"; + + private final DataSourceService dataSourceService; + private final AlarmCommonReduce alarmCommonReduce; + + + public LogPeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce) { + this.alarmCommonReduce = alarmCommonReduce; + this.dataSourceService = dataSourceService; + } + + public void calculate(AlertDefine define) { + if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) { + log.error("Log define {} is disabled or expression is empty", define.getName()); + return; + } + try { + doCalculate(define); + } catch (Exception e) { + log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage()); + } + } + + private void doCalculate(AlertDefine define) { + try { + // Log-based queries are SQL queries with log-specific expressions + List<Map<String, Object>> results = dataSourceService.query(define.getDatasource(), define.getExpr()); + if (CollectionUtils.isEmpty(results)) { + return; + } + afterThresholdRuleMatch(results, define); + } catch (Exception ignored) { + // Ignore the query exception eg: no result, timeout, etc + } + } + + String getAlertMode(AlertDefine alertDefine) { + String mode = null; + if (alertDefine.getLabels() != null) { + mode = alertDefine.getLabels().get(CommonConstants.ALERT_MODE_LABEL); + } + if (mode == null || mode.isEmpty()) { + return CommonConstants.ALERT_MODE_GROUP; // Default to group mode if not specified + } else { + return mode; + } + } + + /** + * Handle alert after threshold rule match + */ + private void afterThresholdRuleMatch(List<Map<String, Object>> alertContext, AlertDefine define) { + // Determine alert mode from configuration + String alertMode = getAlertMode(define); + + long currentTime = System.currentTimeMillis(); + + switch (alertMode) { + case CommonConstants.ALERT_MODE_INDIVIDUAL: + // Generate individual alerts for each matching log + for (Map<String, Object> context : alertContext) { + generateIndividualAlert(define, context, currentTime); + } + break; + + case CommonConstants.ALERT_MODE_GROUP: + // Generate a single alert group for all matching logs + generateGroupAlert(define, alertContext, currentTime); + break; + default: + log.warn("Unknown alert mode for define {}: {}", define.getName(), alertMode); + } + } + + private void generateIndividualAlert(AlertDefine define, Map<String, Object> context, long currentTime) { + + Map<String, String> alertLabels = new HashMap<>(8); + + Map<String, String> commonFingerPrints = createCommonFingerprints(define); + alertLabels.putAll(commonFingerPrints); + addContextToMap(context, alertLabels); + + Map<String, Object> fieldValueMap = createFieldValueMap(context, define); + Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap); + // Create and send group alert + SingleAlert alert = SingleAlert.builder() + .labels(alertLabels) + .annotations(alertAnnotations) + .content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap)) + .status(CommonConstants.ALERT_STATUS_FIRING) + .triggerTimes(1) + .startAt(currentTime) + .activeAt(currentTime) + .build(); + + alarmCommonReduce.reduceAndSendAlarm(alert.clone()); + + log.debug("Generated individual alert for define: {}", define.getName()); + } + + private void addContextToMap(Map<String, Object> context, Map<String, String> alertLabels) { + for (Map.Entry<String, Object> entry : context.entrySet()) { + if (entry.getValue() != null) { + alertLabels.put(entry.getKey(), entry.getValue().toString()); + } + } + } + + private void generateGroupAlert(AlertDefine define, List<Map<String, Object>> alertContext, long currentTime) { + + List<SingleAlert> alerts = new ArrayList<>(alertContext.size()); + + // Create fingerprints for group alert + Map<String, String> commonFingerPrints = createCommonFingerprints(define); + + // Add context information to fingerprints + commonFingerPrints.put(ROWS, String.valueOf(alertContext.size())); + commonFingerPrints.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP); + + for (Map<String, Object> context : alertContext) { + + Map<String, String> alertLabels = new HashMap<>(8); + + alertLabels.putAll(commonFingerPrints); + // add the context to commonFingerPrints + addContextToMap(context, alertLabels); + + Map<String, Object> fieldValueMap = createFieldValueMap(context, define); + Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap); + // Create and send group alert + SingleAlert alert = SingleAlert.builder() + .labels(alertLabels) + .annotations(alertAnnotations) + .content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap)) + .status(CommonConstants.ALERT_STATUS_FIRING) + .triggerTimes(alertContext.size()) + .startAt(currentTime) + .activeAt(currentTime) + .build(); + alerts.add(alert.clone()); + } + alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts); + + log.debug("Generated group alert for define: {} with {} matching data", + define.getName(), alertContext.size()); + } + + + + private Map<String, String> createCommonFingerprints(AlertDefine define) { + Map<String, String> fingerprints = new HashMap<>(8); + fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName()); + fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId())); + + if (define.getLabels() != null) { + fingerprints.putAll(define.getLabels()); + } + + return fingerprints; + } + + private Map<String, Object> createFieldValueMap(Map<String, Object> context, AlertDefine define) { + Map<String, Object> fieldValueMap = new HashMap<>(8); + for (Map.Entry<String, Object> entry : context.entrySet()) { + if (entry.getValue() != null) { + fieldValueMap.put(entry.getKey(), entry.getValue().toString()); + } + } + if (define.getLabels() != null) { + fieldValueMap.putAll(define.getLabels()); + } + + return fieldValueMap; + } + + private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) { + Map<String, String> annotations = new HashMap<>(8); + + if (define.getAnnotations() != null) { + for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) { + annotations.put(entry.getKey(), + AlertTemplateUtil.render(entry.getValue(), fieldValueMap)); + } + } + + return annotations; + } + +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/MetricsPeriodicAlertCalculator.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/MetricsPeriodicAlertCalculator.java new file mode 100644 index 0000000..124c2d5 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/MetricsPeriodicAlertCalculator.java @@ -0,0 +1,186 @@ +/* + * 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.calculate.periodic; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.calculate.AlarmCacheManager; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.DataSourceService; +import org.apache.hertzbeat.alert.util.AlertTemplateUtil; +import org.apache.hertzbeat.alert.util.AlertUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Metrics Periodic Alert Calculator + */ +@Slf4j +@Component +public class MetricsPeriodicAlertCalculator { + + private static final String VALUE = "__value__"; + private static final String TIMESTAMP = "__timestamp__"; + + private final DataSourceService dataSourceService; + private final AlarmCommonReduce alarmCommonReduce; + private final AlarmCacheManager alarmCacheManager; + + public MetricsPeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce, + AlarmCacheManager alarmCacheManager) { + this.alarmCommonReduce = alarmCommonReduce; + this.alarmCacheManager = alarmCacheManager; + this.dataSourceService = dataSourceService; + } + + /** + * Calculate alerts for the given alert definition + * @param define The alert definition to calculate + */ + public void calculate(AlertDefine define) { + if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) { + log.error("Periodic define {} is disabled or expression is empty", define.getName()); + return; + } + + long currentTimeMilli = System.currentTimeMillis(); + try { + doCalculate(define, currentTimeMilli); + } catch (Exception e) { + log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage()); + } + } + + private void doCalculate(AlertDefine define, long currentTimeMilli) { + try { + List<Map<String, Object>> results = dataSourceService.calculate( + define.getDatasource(), + define.getExpr() + ); + // If no match the expr threshold, the results item map {'value': null} should be null and others field keep + // If results has multi list, should trigger multi alert + if (CollectionUtils.isEmpty(results)) { + return; + } + + for (Map<String, Object> result : results) { + Map<String, String> fingerPrints = new HashMap<>(8); + // Here use the alert name as finger, not care the alert name may be changed + fingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId())); + fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName()); + fingerPrints.putAll(define.getLabels()); + for (Map.Entry<String, Object> entry : result.entrySet()) { + if (entry.getValue() != null && !VALUE.equals(entry.getKey()) + && !TIMESTAMP.equals(entry.getKey())) { + fingerPrints.put(entry.getKey(), entry.getValue().toString()); + } + } + + if (result.get(VALUE) == null) { + // Recovery the alert + handleRecoveredAlert(define.getId(), fingerPrints); + continue; + } + + Map<String, Object> fieldValueMap = new HashMap<>(8); + fieldValueMap.putAll(define.getLabels()); + fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, define.getName()); + for (Map.Entry<String, Object> entry : result.entrySet()) { + if (entry.getValue() != null) { + fieldValueMap.put(entry.getKey(), entry.getValue()); + } + } + afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define); + } + } catch (Exception ignored) { + // Ignore the query exception eg: no result, timeout, etc + } + } + + /** + * Handle alert after threshold rule match + */ + private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints, + Map<String, Object> fieldValueMap, AlertDefine define) { + Long defineId = define.getId(); + String fingerprint = AlertUtil.calculateFingerprint(fingerPrints); + SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint); + Map<String, String> labels = new HashMap<>(8); + fieldValueMap.putAll(define.getLabels()); + labels.putAll(fingerPrints); + int requiredTimes = define.getTimes() == null ? 1 : define.getTimes(); + if (existingAlert == null) { + // First time triggering alert, create new alert and set to pending status + SingleAlert newAlert = SingleAlert.builder() + .labels(labels) + .annotations(define.getAnnotations()) + .content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap)) + .status(CommonConstants.ALERT_STATUS_PENDING) + .triggerTimes(1) + .startAt(currentTimeMilli) + .activeAt(currentTimeMilli) + .build(); + + // If required trigger times is 1, set to firing status directly + if (requiredTimes <= 1) { + newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING); + alarmCacheManager.putFiring(defineId, fingerprint, newAlert); + alarmCommonReduce.reduceAndSendAlarm(newAlert.clone()); + } else { + // Otherwise put into pending queue first + alarmCacheManager.putPending(defineId, fingerprint, newAlert); + } + } else { + // Update existing alert + existingAlert.setTriggerTimes(existingAlert.getTriggerTimes() + 1); + existingAlert.setActiveAt(currentTimeMilli); + + // Check if required trigger times reached + if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) { + // Reached trigger times threshold, change to firing status + alarmCacheManager.removePending(defineId, fingerprint); + existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING); + alarmCacheManager.putFiring(defineId, fingerprint, existingAlert); + alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone()); + } + } + } + + /** + * Handle recovered alert + */ + private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) { + String fingerprint = AlertUtil.calculateFingerprint(fingerprints); + SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint); + if (firingAlert != null) { + firingAlert.setTriggerTimes(1); + firingAlert.setEndAt(System.currentTimeMillis()); + firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED); + alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone()); + } + alarmCacheManager.removePending(defineId, fingerprint); + } + +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleScheduler.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleScheduler.java new file mode 100644 index 0000000..49e9004 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleScheduler.java @@ -0,0 +1,242 @@ +/* + * 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.calculate.periodic; + +import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC; +import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC; +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.AlertDefineDao; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; + +/** + * Periodic Alert Rule Scheduler + */ +@Slf4j +@Component +public class PeriodicAlertRuleScheduler implements CommandLineRunner, DisposableBean { + + private final MetricsPeriodicAlertCalculator metricsCalculator; + private final LogPeriodicAlertCalculator logCalculator; + private final AlertDefineDao alertDefineDao; + private final ScheduledExecutorService scheduledExecutor; + private final ExecutorService periodicExecutor; + private final Semaphore periodicPermits; + private final boolean virtualThreadsEnabled; + private final Map<Long, ScheduledTaskState> scheduledTasks; + + public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator, LogPeriodicAlertCalculator logCalculator, AlertDefineDao alertDefineDao) { + this(metricsCalculator, logCalculator, alertDefineDao, VirtualThreadProperties.defaults()); + } + + @Autowired + public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator, + LogPeriodicAlertCalculator logCalculator, + AlertDefineDao alertDefineDao, + VirtualThreadProperties virtualThreadProperties) { + this.metricsCalculator = metricsCalculator; + this.logCalculator = logCalculator; + this.alertDefineDao = alertDefineDao; + Thread.UncaughtExceptionHandler handler = (thread, throwable) -> { + log.error("Scheduled periodic alert threshold has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }; + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("Scheduled periodic alert threshold has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("periodic-alert-threshold-worker-%d") + .build(); + this.scheduledExecutor = Executors.newScheduledThreadPool(10, threadFactory); + VirtualThreadProperties properties = + virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties; + this.virtualThreadsEnabled = properties.enabled(); + int maxConcurrentPeriodicTasks = Math.max(1, properties.alerter().periodicMaxConcurrentJobs()); + this.periodicExecutor = virtualThreadsEnabled + ? Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name("periodic-alert-task-", 0) + .uncaughtExceptionHandler(handler) + .factory()) + : null; + this.periodicPermits = virtualThreadsEnabled ? new Semaphore(maxConcurrentPeriodicTasks) : null; + this.scheduledTasks = new ConcurrentHashMap<>(); + } + + public void cancelSchedule(Long ruleId) { + if (ruleId == null) { + return; + } + ScheduledTaskState state = scheduledTasks.remove(ruleId); + if (state != null) { + state.cancel(); + } + } + + public void updateSchedule(AlertDefine rule) { + if (rule == null || rule.getId() == null) { + log.error("Alert rule is null or rule id is null."); + return; + } + cancelSchedule(rule.getId()); + if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC) + || rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) { + ScheduledTaskState state = new ScheduledTaskState(rule); + ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate( + virtualThreadsEnabled ? state::trigger : () -> executeRule(rule), + 0, rule.getPeriod(), TimeUnit.SECONDS); + state.setScheduledFuture(future); + scheduledTasks.put(rule.getId(), state); + } + } + + @Override + public void run(String... args) throws Exception { + log.info("Starting periodic alert rule scheduler..."); + List<AlertDefine> metricsPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC); + List<AlertDefine> logPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC); + List<AlertDefine> periodicRules = new ArrayList<>(metricsPeriodicRules.size() + logPeriodicRules.size()); + periodicRules.addAll(metricsPeriodicRules); + periodicRules.addAll(logPeriodicRules); + for (AlertDefine rule : periodicRules) { + updateSchedule(rule); + } + } + + @Override + public void destroy() { + scheduledTasks.values().forEach(ScheduledTaskState::cancel); + scheduledTasks.clear(); + scheduledExecutor.shutdownNow(); + if (periodicExecutor != null) { + periodicExecutor.shutdownNow(); + } + } + + private void executeRule(AlertDefine rule) { + if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) { + metricsCalculator.calculate(rule); + } else if (rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) { + logCalculator.calculate(rule); + } + } + + private final class ScheduledTaskState { + + private final AlertDefine rule; + private ScheduledFuture<?> scheduledFuture; + private Future<?> runningFuture; + private boolean running; + private boolean pending; + private boolean cancelled; + + private ScheduledTaskState(AlertDefine rule) { + this.rule = rule; + } + + private synchronized void setScheduledFuture(ScheduledFuture<?> scheduledFuture) { + this.scheduledFuture = scheduledFuture; + } + + private synchronized void trigger() { + if (cancelled) { + return; + } + if (running) { + pending = true; + return; + } + running = true; + submitLocked(); + } + + private synchronized void cancel() { + cancelled = true; + pending = false; + ScheduledFuture<?> periodicFuture = scheduledFuture; + Future<?> currentFuture = runningFuture; + if (periodicFuture != null) { + periodicFuture.cancel(true); + } + if (currentFuture != null) { + currentFuture.cancel(true); + } + } + + private void submitLocked() { + try { + runningFuture = periodicExecutor.submit(() -> { + boolean permitAcquired = false; + try { + periodicPermits.acquire(); + permitAcquired = true; + if (!Thread.currentThread().isInterrupted()) { + executeRule(rule); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + log.error("Periodic alert rule {} execution error: {}", rule.getName(), e.getMessage(), e); + } finally { + if (permitAcquired) { + periodicPermits.release(); + } + onComplete(); + } + }); + } catch (RuntimeException e) { + running = false; + throw e; + } + } + + private synchronized void onComplete() { + runningFuture = null; + if (cancelled) { + running = false; + pending = false; + return; + } + if (!pending) { + running = false; + return; + } + pending = false; + submitLocked(); + } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/MetricsRealTimeAlertCalculator.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/MetricsRealTimeAlertCalculator.java new file mode 100644 index 0000000..f28a48a --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/MetricsRealTimeAlertCalculator.java @@ -0,0 +1,446 @@ +/* + * 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.calculate.realtime; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.AlerterWorkerPool; +import org.apache.hertzbeat.alert.calculate.AlarmCacheManager; +import org.apache.hertzbeat.alert.calculate.JexlExprCalculator; +import org.apache.hertzbeat.alert.dao.SingleAlertDao; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.alert.util.AlertTemplateUtil; +import org.apache.hertzbeat.alert.util.AlertUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException; +import org.apache.hertzbeat.common.util.BackoffUtils; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.ExponentialBackoff; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Calculate alarms based on the alarm definition rules and collected data + */ +@Component +@Slf4j +public class MetricsRealTimeAlertCalculator { + + private static final int CALCULATE_THREADS = 3; + + private static final String KEY_INSTANCE = "__instance__"; + private static final String KEY_INSTANCE_NAME = "__instancename__"; + private static final String KEY_INSTANCE_HOST = "__instancehost__"; + private static final String KEY_APP = "__app__"; + private static final String KEY_METRICS = "__metrics__"; + private static final String KEY_PRIORITY = "__priority__"; + private static final String KEY_CODE = "__code__"; + private static final String KEY_AVAILABLE = "__available__"; + private static final String KEY_LABELS = "__labels__"; + private static final String UP = "up"; + private static final String DOWN = "down"; + private static final String KEY_ROW = "__row__"; + + private static final Pattern APP_PATTERN = Pattern.compile("equals\\(__app__,\"([^\"]+)\"\\)"); + private static final Pattern AVAILABLE_PATTERN = Pattern.compile("equals\\(__available__,\"([^\"]+)\"\\)"); + private static final Pattern LABEL_PATTERN = Pattern.compile("contains\\(__labels__,\\s*\"([^\"]+)\"\\)"); + private static final Pattern INSTANCE_PATTERN = Pattern.compile("equals\\(__instance__,\\s*\"(\\d+)\"\\)"); + private static final Pattern METRICS_PATTERN = Pattern.compile("equals\\(__metrics__,\"([^\"]+)\"\\)"); + + private final AlerterWorkerPool workerPool; + private final CommonDataQueue dataQueue; + private final AlertDefineService alertDefineService; + private final AlarmCommonReduce alarmCommonReduce; + private final AlarmCacheManager alarmCacheManager; + private final JexlExprCalculator jexlExprCalculator; + + + @Autowired + public MetricsRealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue, + AlertDefineService alertDefineService, SingleAlertDao singleAlertDao, + AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager, + JexlExprCalculator jexlExprCalculator) { + this(workerPool, dataQueue, alertDefineService, singleAlertDao, alarmCommonReduce, alarmCacheManager, jexlExprCalculator, true); + } + + /** + * Constructor for MetricsRealTimeAlertCalculator with a toggle to control whether to start alert calculation threads. + * + * @param workerPool The worker pool used for concurrent alert calculation. + * @param dataQueue The queue from which metric data is pulled and pushed. + * @param alertDefineService The service providing alert definition rules. + * @param singleAlertDao The DAO for fetching persisted alert states from storage. + * @param alarmCommonReduce The component responsible for reducing and sending alerts. + * @param alarmCacheManager The cache manager for managing alert states. + * @param start If true, the alert calculation threads will start automatically; + * set to false to disable thread start (useful for unit testing). + */ + public MetricsRealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue, + AlertDefineService alertDefineService, SingleAlertDao singleAlertDao, + AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager, + JexlExprCalculator jexlExprCalculator, boolean start) { + this.workerPool = workerPool; + this.dataQueue = dataQueue; + this.alarmCommonReduce = alarmCommonReduce; + this.alertDefineService = alertDefineService; + this.alarmCacheManager = alarmCacheManager; + this.jexlExprCalculator = jexlExprCalculator; + if (start) { + startCalculate(); + } + } + + /** + * Start the alert calculation threads + */ + public void startCalculate() { + Runnable runnable = () -> { + ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L); + while (!Thread.currentThread().isInterrupted()) { + try { + CollectRep.MetricsData metricsData = dataQueue.pollMetricsDataToAlerter(); + if (metricsData == null) { + continue; + } + backoff.reset(); + calculate(metricsData); + dataQueue.sendMetricsDataToStorage(metricsData); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } catch (CommonDataQueueUnknownException ue) { + if (!BackoffUtils.shouldContinueAfterBackoff(backoff)) { + break; + } + } catch (Exception e) { + log.error("calculate alarm error: {}.", e.getMessage(), e); + } + } + }; + for (int i = 0; i < CALCULATE_THREADS; i++) { + workerPool.executeJob(runnable); + } + } + + protected void calculate(CollectRep.MetricsData metricsData) { + long currentTimeMilli = System.currentTimeMillis(); + String instance = String.valueOf(metricsData.getId()); + String instanceName = metricsData.getInstanceName(); + String instanceHost = metricsData.getInstance(); + String app = metricsData.getApp(); + String metrics = metricsData.getMetrics(); + if ((CommonConstants.PROMETHEUS_APP_PREFIX + instanceName).equals(metricsData.getApp())) { + app = CommonConstants.PROMETHEUS; + } + int priority = metricsData.getPriority(); + int code = metricsData.getCode().getNumber(); + Map<String, String> labels = metricsData.getLabels(); + Map<String, String> annotations = metricsData.getAnnotations(); + List<AlertDefine> thresholds = this.alertDefineService.getMetricsRealTimeAlertDefines(); + // Filter thresholds by app, metrics, labels and instance + thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, labels, instance, priority); + if (thresholds.isEmpty()) { + return; + } + Map<String, Object> commonContext = new HashMap<>(8); + commonContext.put(KEY_INSTANCE, instance); + commonContext.put(KEY_INSTANCE_NAME, instanceName); + commonContext.put(KEY_INSTANCE_HOST, instanceHost); + commonContext.put(KEY_APP, app); + commonContext.put(KEY_PRIORITY, priority); + commonContext.put(KEY_CODE, code); + commonContext.put(KEY_METRICS, metrics); + commonContext.put(KEY_LABELS, String.join(",", kvLabelsToKvStringSet(labels))); + if (priority == 0) { + commonContext.put(KEY_AVAILABLE, metricsData.getCode() == CollectRep.Code.SUCCESS ? UP : DOWN); + } + if (labels != null) { + commonContext.putAll(labels); + } + List<CollectRep.Field> fields = metricsData.getFields(); + Map<String, Object> fieldValueMap = new HashMap<>(8); + int valueRowCount = metricsData.getValuesCount(); + for (AlertDefine define : thresholds) { + if (define.getLabels() == null) { + define.setLabels(new HashMap<>(8)); + } + if (define.getAnnotations() == null) { + define.setAnnotations(new HashMap<>(8)); + } + fieldValueMap.clear(); + fieldValueMap.putAll(commonContext); + final String expr = define.getExpr(); + if (StringUtils.isBlank(expr)) { + continue; + } + Long defineId = define.getId(); + Map<String, String> commonFingerPrints = new HashMap<>(8); + // here use the alert name as finger, not care the alert name may be changed + commonFingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId())); + commonFingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName()); + commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_NAME, instanceName); + commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instanceHost); + commonFingerPrints.putAll(define.getLabels()); + if (labels != null) { + commonFingerPrints.putAll(labels); + } + { + // trigger the expr before the metrics data, due the available up down or others + try { + boolean match = jexlExprCalculator.execAlertExpression(fieldValueMap, expr, true); + try { + if (match) { + // If the threshold rule matches, the number of times the threshold has been triggered is determined and an alarm is triggered + afterThresholdRuleMatch(defineId, currentTimeMilli, commonFingerPrints, fieldValueMap, define, annotations); + } else { + handleRecoveredAlert(defineId, commonFingerPrints); + } + // if this threshold pre compile success, ignore blew + continue; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } catch (Exception ignored) {} + } + Map<String, String> fingerPrints = new HashMap<>(8); + for (CollectRep.ValueRow valueRow : metricsData.getValues()) { + if (CollectionUtils.isEmpty(valueRow.getColumnsList())) { + continue; + } + fieldValueMap.clear(); + fieldValueMap.put(KEY_ROW, valueRowCount); + fieldValueMap.putAll(commonContext); + fingerPrints.clear(); + fingerPrints.putAll(commonFingerPrints); + for (int index = 0; index < valueRow.getColumnsList().size(); index++) { + String valueStr = valueRow.getColumns(index); + final CollectRep.Field field = fields.get(index); + if (CommonConstants.NULL_VALUE.equals(valueStr)) { + fieldValueMap.put(field.getName(), null); + continue; + } + final int fieldType = field.getType(); + + if (fieldType == CommonConstants.TYPE_NUMBER) { + final Double doubleValue; + if ((doubleValue = CommonUtil.parseStrDouble(valueStr)) != null) { + fieldValueMap.put(field.getName(), doubleValue); + } + } else if (fieldType == CommonConstants.TYPE_TIME) { + final Integer integerValue; + if ((integerValue = CommonUtil.parseStrInteger(valueStr)) != null) { + fieldValueMap.put(field.getName(), integerValue); + } + } else { + if (StringUtils.isNotEmpty(valueStr)) { + fieldValueMap.put(field.getName(), valueStr); + } + } + + if (field.getLabel()) { + fingerPrints.put(field.getName(), valueStr); + } + } + try { + boolean match = jexlExprCalculator.execAlertExpression(fieldValueMap, expr, false); + try { + if (match) { + afterThresholdRuleMatch(defineId, currentTimeMilli, fingerPrints, fieldValueMap, define, annotations); + } else { + handleRecoveredAlert(defineId, fingerPrints); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } catch (Exception ignored) {} + } + } + } + + /** + * Filter alert definitions by app, metrics and instance + * + * @param thresholds Alert definitions to filter + * @param app Current app name + * @param metrics Current metrics name + * @param labels Current labels + * @param instance Current instance id + * @param priority Current priority + * @return Filtered alert definitions + */ + public List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, Map<String, String> labels, String instance, int priority) { + return thresholds.stream() + .filter(define -> { + if (StringUtils.isBlank(define.getExpr())) { + return false; + } + String expr = define.getExpr(); + + // Extract and check app - required + Matcher appMatcher = APP_PATTERN.matcher(expr); + if (!appMatcher.find() || !app.equals(appMatcher.group(1))) { + return false; + } + + // Extract and check available - required + if (priority != 0) { + Matcher availableMatcher = AVAILABLE_PATTERN.matcher(expr); + if (availableMatcher.find()) { + return false; + } + } + + // Extract and check metrics - optional + Matcher metricsMatcher = METRICS_PATTERN.matcher(expr); + if (metricsMatcher.find() && !metrics.equals(metricsMatcher.group(1))) { + return false; + } + + // Extract and check instance - optional with multiple values + Matcher instanceMatcher = INSTANCE_PATTERN.matcher(expr); + Matcher labelMatcher = LABEL_PATTERN.matcher(expr); + // If no instance and instance labels specified in expr, accept all instances + if (!instanceMatcher.find() && !labelMatcher.find()) { + return true; + } + + // Reset matcher to check all instances + instanceMatcher.reset(); + labelMatcher.reset(); + // If instances specified, current instance must match one of them + while (instanceMatcher.find()) { + if (Objects.equals(instance, instanceMatcher.group(1))) { + return true; + } + } + // If instance labels specified, current instance must match one of them + Set<String> labelKvStringSet = kvLabelsToKvStringSet(labels); + while (labelMatcher.find()) { + String label = labelMatcher.group(1); + if (labelKvStringSet.contains(label)) { + return true; + } + } + return false; + }) + .collect(Collectors.toList()); + } + + /** + * Handle recovered alert + */ + private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) { + String fingerprint = AlertUtil.calculateFingerprint(fingerprints); + SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint); + if (firingAlert != null) { + // todo consider multi times to tig for resolved alert + firingAlert.setTriggerTimes(1); + firingAlert.setEndAt(System.currentTimeMillis()); + firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED); + alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone()); + } + alarmCacheManager.removePending(defineId, fingerprint); + } + + + /** + * Handle alert after threshold rule match + */ + private void afterThresholdRuleMatch(long defineId, long currentTimeMilli, Map<String, String> fingerPrints, + Map<String, Object> fieldValueMap, AlertDefine define, + Map<String, String> annotations) { + // fingerprint for the padding cache + String fingerprint = AlertUtil.calculateFingerprint(fingerPrints); + SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint); + fieldValueMap.putAll(define.getLabels()); + int requiredTimes = define.getTimes() == null ? 1 : define.getTimes(); + if (existingAlert == null) { + // First time triggering alert, create new alert and set to pending status + Map<String, String> alertLabels = new HashMap<>(8); + alertLabels.putAll(fingerPrints); + Map<String, String> alertAnnotations = new HashMap<>(8); + if (annotations != null) { + alertAnnotations.putAll(annotations); + } + if (define.getAnnotations() != null) { + alertAnnotations.putAll(define.getAnnotations()); + } + // render var content in annotations + for (Map.Entry<String, String> entry : alertAnnotations.entrySet()) { + entry.setValue(AlertTemplateUtil.render(entry.getValue(), fieldValueMap)); + } + SingleAlert newAlert = SingleAlert.builder() + .labels(alertLabels) + .annotations(alertAnnotations) + // render var content in content + .content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap)) + .status(CommonConstants.ALERT_STATUS_PENDING) + .triggerTimes(1) + .startAt(currentTimeMilli) + .activeAt(currentTimeMilli) + .build(); + + // If required trigger times is 1, set to firing status directly + if (requiredTimes <= 1) { + newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING); + alarmCacheManager.putFiring(defineId, fingerprint, newAlert); + alarmCommonReduce.reduceAndSendAlarm(newAlert.clone()); + } else { + // Otherwise put into pending queue first + alarmCacheManager.putPending(define.getId(), fingerprint, newAlert); + } + } else { + // Update existing alert + existingAlert.setTriggerTimes(existingAlert.getTriggerTimes() + 1); + existingAlert.setActiveAt(currentTimeMilli); + + // Check if required trigger times reached + if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) { + // Reached trigger times threshold, change to firing status + alarmCacheManager.removePending(defineId, fingerprint); + existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING); + alarmCacheManager.putFiring(defineId, fingerprint, existingAlert); + alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone()); + } + } + } + + private Set<String> kvLabelsToKvStringSet(Map<String, String> labels) { + if (labels == null || labels.isEmpty()) { + return Collections.singleton(""); + } + return labels.entrySet().stream() + .map(item -> item.getKey() + ":" + item.getValue()) + .collect(Collectors.toSet()); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/WindowedLogRealTimeAlertCalculator.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/WindowedLogRealTimeAlertCalculator.java new file mode 100644 index 0000000..5a71d5f --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/WindowedLogRealTimeAlertCalculator.java @@ -0,0 +1,150 @@ +/* + * 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.calculate.realtime; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.calculate.realtime.window.LogWorker; +import org.apache.hertzbeat.alert.calculate.realtime.window.TimeService; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException; +import org.apache.hertzbeat.common.util.BackoffUtils; +import org.apache.hertzbeat.common.util.ExponentialBackoff; +import org.springframework.stereotype.Component; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * WindowedLogRealTimeAlertCalculator - Single entry point for log stream processing + * Responsible for: + * 1. Reading from original log stream + * 2. Extracting event timestamps + * 3. Maintaining maxTimestamp for watermark generation + * 4. Distributing logs to workers + */ +@Component +@Slf4j +public class WindowedLogRealTimeAlertCalculator implements Runnable { + + private static final int CALCULATE_THREADS = 3; + + private final CommonDataQueue dataQueue; + private final TimeService timeService; + private ThreadPoolExecutor dispatcherExecutor; + private final LogWorker logWorker; + + public WindowedLogRealTimeAlertCalculator(CommonDataQueue dataQueue, TimeService timeService, LogWorker logWorker) { + this.dataQueue = dataQueue; + this.timeService = timeService; + this.logWorker = logWorker; + } + + @Override + public void run() { + ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L); + while (!Thread.currentThread().isInterrupted()) { + try { + LogEntry logEntry = dataQueue.pollLogEntry(); + if (logEntry == null) { + continue; + } + backoff.reset(); + processLogEntry(logEntry); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (CommonDataQueueUnknownException ue) { + if (!BackoffUtils.shouldContinueAfterBackoff(backoff)) { + break; + } + } catch (Exception e) { + log.error("Error in log dispatch loop: {}", e.getMessage(), e); + } + } + } + + private void processLogEntry(LogEntry logEntry) { + // Extract event timestamp + long eventTimestamp = extractEventTimestamp(logEntry); + + // Check if timestamp is within reasonable range + if (!timeService.isValidTimestamp(eventTimestamp)) { + log.warn("Dropping log with invalid timestamp: {}", eventTimestamp); + return; + } + + // Check if this is late data based on current watermark + if (timeService.isLateData(eventTimestamp)) { + log.warn("Dropping late data, timestamp: {}, watermark: {}", + eventTimestamp, timeService.getCurrentWatermark()); + return; + } + + // Update max timestamp (only validated timestamps can update watermark) + timeService.updateMaxTimestamp(eventTimestamp); + logWorker.reduceAndSendLogTask(logEntry); + } + + private long extractEventTimestamp(LogEntry logEntry) { + if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) { + return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds + } + if (logEntry.getObservedTimeUnixNano() != null && logEntry.getObservedTimeUnixNano() != 0) { + return logEntry.getObservedTimeUnixNano() / 1_000_000; // Convert to milliseconds + } + return System.currentTimeMillis(); + } + + @PostConstruct + public void start() { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("Alerter workerExecutor has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("log-dispatcher-%d") + .build(); + // Create dispatcher thread executor + this.dispatcherExecutor = new ThreadPoolExecutor( + CALCULATE_THREADS, + CALCULATE_THREADS, + 10, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + threadFactory, + new ThreadPoolExecutor.AbortPolicy() + ); + for (int i = 0; i < CALCULATE_THREADS; i++) { + dispatcherExecutor.execute(this); + } + } + + @PreDestroy + public void stop() { + if (dispatcherExecutor != null) { + dispatcherExecutor.shutdownNow(); + } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluator.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluator.java new file mode 100644 index 0000000..e85b145 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluator.java @@ -0,0 +1,335 @@ +/* + * 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.calculate.realtime.window; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.common.concurrent.ManagedExecutor; +import org.apache.hertzbeat.common.concurrent.ManagedExecutors; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import org.apache.hertzbeat.alert.util.AlertTemplateUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.entity.log.LogEntry; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Alarm Evaluator - Final alarm logic trigger + * Responsible for: + * 1. Receiving closed window data from WindowAggregator + * 2. Applying alert logic (count threshold, alert mode) + * 3. Generating individual alerts or alert groups + * 4. Sending alerts to AlarmCommonReduce + */ +@Component +@Slf4j +public class AlarmEvaluator implements DisposableBean { + + private static final String WINDOW_START_TIME = "window_start_time"; + private static final String WINDOW_END_TIME = "window_end_time"; + private static final String MATCHING_LOGS_COUNT = "matching_logs_count"; + + private final AlarmCommonReduce alarmCommonReduce; + private final ManagedExecutor workerExecutor; + + public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce) { + this(alarmCommonReduce, VirtualThreadProperties.defaults()); + } + + @Autowired + public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce, VirtualThreadProperties virtualThreadProperties) { + this.alarmCommonReduce = alarmCommonReduce; + VirtualThreadProperties properties = + virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties; + this.workerExecutor = initAlarmEvaluator(properties); + } + + public ManagedExecutor initAlarmEvaluator(VirtualThreadProperties properties) { + Thread.UncaughtExceptionHandler handler = (thread, throwable) -> { + log.error("alerter-reduce-worker has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }; + if (properties.enabled()) { + VirtualThreadProperties.QueueProperties queueProperties = properties.alerter().windowEvaluator(); + return ManagedExecutors.newQueuedVirtualExecutor("alerter-window-evaluator", "alerter-window-evaluator-", + queueProperties.maxConcurrentJobs(), queueProperties.queueCapacity(), handler); + } + return ManagedExecutors.wrap("alerter-window-evaluator", new java.util.concurrent.ThreadPoolExecutor(2, + 10, + 10, + java.util.concurrent.TimeUnit.SECONDS, + new java.util.concurrent.LinkedBlockingQueue<>(), + new ThreadFactoryBuilder() + .setUncaughtExceptionHandler(handler) + .setDaemon(true) + .setNameFormat("alerter-reduce-worker-%d") + .build(), + new java.util.concurrent.ThreadPoolExecutor.AbortPolicy())); + } + + public void sendAndProcessWindowData(WindowAggregator.WindowData windowData) { + workerExecutor.execute(processWindowData(windowData)); + } + + private Runnable processWindowData(WindowAggregator.WindowData windowData) { + return () -> { + AlertDefine alertDefine = windowData.getAlertDefine(); + List<MatchingLogEvent> matchingLogs = windowData.getMatchingLogs(); + + if (matchingLogs.isEmpty()) { + return; + } + + // Check if count threshold is met + int requiredTimes = alertDefine.getTimes() != null ? alertDefine.getTimes() : 1; + if (matchingLogs.size() < requiredTimes) { + log.debug("Window {} has {} matching logs, but requires {} times", + windowData.getWindowKey(), matchingLogs.size(), requiredTimes); + return; + } + + // Determine alert mode from configuration + String alertMode = getAlertMode(alertDefine); + + long currentTime = System.currentTimeMillis(); + + switch (alertMode) { + case CommonConstants.ALERT_MODE_INDIVIDUAL: + // Generate individual alerts for each matching log + for (MatchingLogEvent matchingLog : matchingLogs) { + generateIndividualAlert(matchingLog, currentTime); + } + break; + + case CommonConstants.ALERT_MODE_GROUP: + // Generate a single alert group for all matching logs + generateGroupAlert(windowData, matchingLogs, currentTime); + break; + default: + log.warn("Unknown alert mode for define {}: {}", alertDefine.getName(), alertMode); + } + }; + } + + private void generateIndividualAlert(MatchingLogEvent matchingLog, long currentTime) { + AlertDefine define = matchingLog.getAlertDefine(); + LogEntry logEntry = matchingLog.getLogEntry(); + + Map<String, String> alertLabels = new HashMap<>(8); + + // Create fingerprints for group alert + Map<String, String> commonFingerPrints = createCommonFingerprints(define); + alertLabels.putAll(commonFingerPrints); + // add the log data to commonFingerPrints + addLogEntryToMap(logEntry, alertLabels); + + Map<String, Object> fieldValueMap = createFieldValueMap(logEntry, define); + Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap); + // Create and send group alert + SingleAlert alert = SingleAlert.builder() + .labels(alertLabels) + .annotations(alertAnnotations) + .content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap)) + .status(CommonConstants.ALERT_STATUS_FIRING) + .triggerTimes(1) + .startAt(currentTime) + .activeAt(currentTime) + .build(); + + alarmCommonReduce.reduceAndSendAlarm(alert.clone()); + + log.debug("Generated individual alert for define: {}", define.getName()); + } + + private void generateGroupAlert(WindowAggregator.WindowData windowData, + List<MatchingLogEvent> matchingLogs, long currentTime) { + + List<SingleAlert> alerts = new ArrayList<>(matchingLogs.size()); + AlertDefine define = windowData.getAlertDefine(); + + // Create fingerprints for group alert + Map<String, String> commonFingerPrints = createCommonFingerprints(define); + + // Add window information to fingerprints + commonFingerPrints.put(WINDOW_START_TIME, String.valueOf(windowData.getStartTime())); + commonFingerPrints.put(WINDOW_END_TIME, String.valueOf(windowData.getEndTime())); + commonFingerPrints.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP); + commonFingerPrints.put(MATCHING_LOGS_COUNT, String.valueOf(matchingLogs.size())); + + for (MatchingLogEvent event: matchingLogs) { + LogEntry logEntry = event.getLogEntry(); + + Map<String, String> alertLabels = new HashMap<>(8); + + alertLabels.putAll(commonFingerPrints); + // add the log data to commonFingerPrints + addLogEntryToMap(logEntry, alertLabels); + + Map<String, Object> fieldValueMap = createFieldValueMap(logEntry, define); + Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap); + // Create and send group alert + SingleAlert alert = SingleAlert.builder() + .labels(alertLabels) + .annotations(alertAnnotations) + .content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap)) + .status(CommonConstants.ALERT_STATUS_FIRING) + .triggerTimes(matchingLogs.size()) + .startAt(currentTime) + .activeAt(currentTime) + .build(); + alerts.add(alert.clone()); + } + alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts); + + log.debug("Generated group alert for define: {} with {} matching logs", + define.getName(), matchingLogs.size()); + } + + private String getAlertMode(AlertDefine alertDefine) { + String mode = null; + if (alertDefine.getLabels() != null) { + mode = alertDefine.getLabels().get(CommonConstants.ALERT_MODE_LABEL); + } + if (mode == null || mode.isEmpty()) { + return CommonConstants.ALERT_MODE_GROUP; // Default to group mode if not specified + } else { + return mode; + } + } + + private Map<String, String> createCommonFingerprints(AlertDefine define) { + Map<String, String> fingerprints = new HashMap<>(8); + fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName()); + fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId())); + + if (define.getLabels() != null) { + fingerprints.putAll(define.getLabels()); + } + + return fingerprints; + } + + private Map<String, Object> createFieldValueMap(LogEntry logEntry, AlertDefine define) { + Map<String, Object> fieldValueMap = new HashMap<>(8); + fieldValueMap.put("log", logEntry); + + if (define.getLabels() != null) { + fieldValueMap.putAll(define.getLabels()); + } + + return fieldValueMap; + } + + private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) { + Map<String, String> annotations = new HashMap<>(8); + + if (define.getAnnotations() != null) { + for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) { + annotations.put(entry.getKey(), + AlertTemplateUtil.render(entry.getValue(), fieldValueMap)); + } + } + + return annotations; + } + + /** + * Add the content from LogEntry object (except timestamp) to commonFingerPrints + * + * @param logEntry log entry object + * @param context context + */ + private void addLogEntryToMap(LogEntry logEntry, Map<String, String> context) { + // Add basic fields + if (logEntry.getSeverityNumber() != null) { + context.put("severityNumber", String.valueOf(logEntry.getSeverityNumber())); + } + if (logEntry.getSeverityText() != null) { + context.put("severityText", logEntry.getSeverityText()); + } + if (logEntry.getBody() != null) { + context.put("body", String.valueOf(logEntry.getBody())); + } + if (logEntry.getDroppedAttributesCount() != null) { + context.put("droppedAttributesCount", String.valueOf(logEntry.getDroppedAttributesCount())); + } + if (logEntry.getTraceId() != null) { + context.put("traceId", logEntry.getTraceId()); + } + if (logEntry.getSpanId() != null) { + context.put("spanId", logEntry.getSpanId()); + } + if (logEntry.getTraceFlags() != null) { + context.put("traceFlags", String.valueOf(logEntry.getTraceFlags())); + } + + // Add attributes + if (logEntry.getAttributes() != null && !logEntry.getAttributes().isEmpty()) { + for (Map.Entry<String, Object> entry : logEntry.getAttributes().entrySet()) { + if (entry.getValue() != null) { + context.put("attr_" + entry.getKey(), String.valueOf(entry.getValue())); + } + } + } + + // Add resource + if (logEntry.getResource() != null && !logEntry.getResource().isEmpty()) { + for (Map.Entry<String, Object> entry : logEntry.getResource().entrySet()) { + if (entry.getValue() != null) { + context.put("resource_" + entry.getKey(), String.valueOf(entry.getValue())); + } + } + } + + // Add instrumentationScope + if (logEntry.getInstrumentationScope() != null) { + LogEntry.InstrumentationScope scope = logEntry.getInstrumentationScope(); + if (scope.getName() != null) { + context.put("scope_name", scope.getName()); + } + if (scope.getVersion() != null) { + context.put("scope_version", scope.getVersion()); + } + if (scope.getDroppedAttributesCount() != null) { + context.put("scope_droppedAttributesCount", String.valueOf(scope.getDroppedAttributesCount())); + } + if (scope.getAttributes() != null && !scope.getAttributes().isEmpty()) { + for (Map.Entry<String, Object> entry : scope.getAttributes().entrySet()) { + if (entry.getValue() != null) { + context.put("scope_attr_" + entry.getKey(), String.valueOf(entry.getValue())); + } + } + } + } + } + + @Override + public void destroy() { + workerExecutor.close(); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/LogWorker.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/LogWorker.java new file mode 100644 index 0000000..16f25c6 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/LogWorker.java @@ -0,0 +1,123 @@ +/* + * 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.calculate.realtime.window; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.AlerterWorkerPool; +import org.apache.hertzbeat.alert.calculate.JexlExprCalculator; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * LogWorker Responsible for: + * 1. Receiving logs from WindowedLogRealTimeAlertCalculator + * 2. Evaluating alert expressions against logs + * 3. Sending matching logs to WindowAggregator + */ +@Slf4j +@Component +public class LogWorker { + + private static final String LOG_PREFIX = "log"; + + private final AlertDefineService alertDefineService; + private final JexlExprCalculator jexlExprCalculator; + private final WindowAggregator windowAggregator; + private final AlerterWorkerPool workerPool; + + @Autowired + public LogWorker(AlertDefineService alertDefineService, + JexlExprCalculator jexlExprCalculator, WindowAggregator windowAggregator, + AlerterWorkerPool workerPool) { + this.alertDefineService = alertDefineService; + this.jexlExprCalculator = jexlExprCalculator; + this.windowAggregator = windowAggregator; + this.workerPool = workerPool; + } + + public void reduceAndSendLogTask(LogEntry logEntry) { + workerPool.executeLogJob(reduceLogTask(logEntry)); + } + + public Runnable reduceLogTask(LogEntry logEntry) { + return () -> { + try { + processLogEntry(logEntry); + } catch (Exception e) { + log.error("Error processing log entry in worker: {}", e.getMessage(), e); + } + }; + } + + private void processLogEntry(LogEntry logEntry) { + // Get all log alert definitions + List<AlertDefine> alertDefines = alertDefineService.getLogRealTimeAlertDefines(); + + // Create context for expression evaluation + Map<String, Object> context = new HashMap<>(8); + context.put(LOG_PREFIX, logEntry); + + // Process each alert definition + for (AlertDefine define : alertDefines) { + if (define.getExpr() == null || define.getExpr().trim().isEmpty()) { + continue; + } + + try { + // Evaluate alert expression + boolean match = jexlExprCalculator.execAlertExpression(context, define.getExpr(), false); + + if (match) { + // Create matching log event and send to WindowAggregator + MatchingLogEvent event = createMatchingLogEvent(logEntry, define); + windowAggregator.addMatchingLog(event); + } + } catch (Exception e) { + log.warn("Error evaluating expression for alert define {}: {}", define.getName(), e.getMessage()); + } + } + } + + private MatchingLogEvent createMatchingLogEvent(LogEntry logEntry, AlertDefine define) { + long eventTimestamp = extractEventTimestamp(logEntry); + + return MatchingLogEvent.builder() + .logEntry(logEntry) + .alertDefine(define) + .eventTimestamp(eventTimestamp) + .workerTimestamp(System.currentTimeMillis()) + .build(); + } + + private long extractEventTimestamp(LogEntry logEntry) { + if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) { + return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds + } + if (logEntry.getObservedTimeUnixNano() != null && logEntry.getObservedTimeUnixNano() != 0) { + return logEntry.getObservedTimeUnixNano() / 1_000_000; // Convert to milliseconds + } + return System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/MatchingLogEvent.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/MatchingLogEvent.java new file mode 100644 index 0000000..34f9add --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/MatchingLogEvent.java @@ -0,0 +1,40 @@ +/* + * 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.calculate.realtime.window; + +import lombok.Builder; +import lombok.Data; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.log.LogEntry; + +/** + * Represents a log entry that matched an alert expression + * Used for communication between LogWorker and WindowAggregator + */ +@Data +@Builder +public class MatchingLogEvent { + + private LogEntry logEntry; + + private AlertDefine alertDefine; + + private long eventTimestamp; + + private long workerTimestamp; +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/TimeService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/TimeService.java new file mode 100644 index 0000000..d7d4d82 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/TimeService.java @@ -0,0 +1,213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.calculate.realtime.window; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import jakarta.annotation.PostConstruct; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PreDestroy; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Watermark Generator responsible for: + * 1. Receiving maxTimestamp updates from WindowedLogRealTimeAlertCalculator + * 2. Calculating watermarks based on configurable delay + * 3. Broadcasting watermarks to all subscribers (WindowAggregator) + */ +@Component +@Slf4j +public class TimeService { + + static final long DEFAULT_WATERMARK_DELAY_MS = 30_000; // 30 seconds + private static final long WATERMARK_BROADCAST_INTERVAL_MS = 5_000; // 5 seconds + + // Define acceptable timestamp range to filter abnormal timestamps + private static final long MAX_FUTURE_TIME_MS = 60_000; // Allow 1 minute future time + private static final long MAX_PAST_TIME_MS = 24 * 60 * 60 * 1000; // Allow 24 hours past time + + private final AtomicLong maxTimestamp = new AtomicLong(0); + private final AtomicLong currentWatermark = new AtomicLong(0); + private final CopyOnWriteArrayList<WatermarkListener> listeners = new CopyOnWriteArrayList<>(); + private ScheduledExecutorService scheduler; + + public TimeService(List<WatermarkListener> initialListeners) { + listeners.addAll(initialListeners); + } + + @PostConstruct + public void start() { + // Create internal scheduled executor + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("TimeService scheduler has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("timeservice-scheduler-%d") + .build(); + + this.scheduler = Executors.newSingleThreadScheduledExecutor(threadFactory); + + // Start watermark broadcast scheduler + scheduler.scheduleAtFixedRate( + this::broadcastWatermark, + 0, + WATERMARK_BROADCAST_INTERVAL_MS, + TimeUnit.MILLISECONDS + ); + + log.info("TimeService started with watermark delay: {}ms", DEFAULT_WATERMARK_DELAY_MS); + } + + @PreDestroy + public void stop() { + if (scheduler != null && !scheduler.isShutdown()) { + log.info("Shutting down TimeService scheduler..."); + scheduler.shutdown(); + try { + if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) { + log.warn("TimeService scheduler did not terminate within 10 seconds, forcing shutdown"); + scheduler.shutdownNow(); + if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { + log.error("TimeService scheduler did not terminate"); + } + } + } catch (InterruptedException e) { + log.warn("Interrupted while waiting for TimeService scheduler to terminate"); + scheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + log.info("TimeService stopped"); + } + + /** + * Check if timestamp is within acceptable range + */ + public boolean isValidTimestamp(long timestamp) { + long currentTime = System.currentTimeMillis(); + return timestamp >= (currentTime - MAX_PAST_TIME_MS) && timestamp <= (currentTime + MAX_FUTURE_TIME_MS); + } + + /** + * Check if data is late based on current watermark + */ + public boolean isLateData(long timestamp) { + return timestamp < getCurrentWatermark(); + } + + /** + * Update max timestamp from WindowedLogRealTimeAlertCalculator + */ + public void updateMaxTimestamp(long timestamp) { + maxTimestamp.getAndUpdate(current -> Math.max(current, timestamp)); + } + + /** + * Add watermark listener + */ + public void addWatermarkListener(WatermarkListener listener) { + listeners.add(listener); + } + + /** + * Remove watermark listener + */ + public void removeWatermarkListener(WatermarkListener listener) { + listeners.remove(listener); + } + + /** + * Get current watermark + */ + public long getCurrentWatermark() { + return currentWatermark.get(); + } + + /** + * Get max timestamp + */ + public long getMaxTimestamp() { + return maxTimestamp.get(); + } + + /** + * Calculate and broadcast watermark + */ + private void broadcastWatermark() { + + try { + long maxTs = maxTimestamp.get(); + if (maxTs <= 0) { + return; + } + // Calculate watermark: maxTimestamp - delay + long newWatermark = maxTs - DEFAULT_WATERMARK_DELAY_MS; + long currentWm = currentWatermark.get(); + + // Only advance watermark (monotonic property) + if (newWatermark <= currentWm) { + return; + } + if (currentWatermark.compareAndSet(currentWm, newWatermark)) { + // Broadcast to all listeners + Watermark watermark = new Watermark(newWatermark); + for (WatermarkListener listener : listeners) { + try { + listener.onWatermark(watermark); + } catch (Exception e) { + log.error("Error notifying watermark listener: {}", e.getMessage(), e); + } + } + log.debug("Broadcast watermark: {} (maxTimestamp: {})", newWatermark, maxTs); + } + } catch (Exception e) { + log.error("Error in watermark broadcast: {}", e.getMessage(), e); + } + } + + /** + * Watermark data class + */ + @Data + @AllArgsConstructor + @Getter + public static class Watermark { + private final long timestamp; + } + + /** + * Interface for watermark listeners + */ + public interface WatermarkListener { + void onWatermark(Watermark watermark); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/WindowAggregator.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/WindowAggregator.java new file mode 100644 index 0000000..5fd4ca6 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/calculate/realtime/window/WindowAggregator.java @@ -0,0 +1,251 @@ +/* + * 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.calculate.realtime.window; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import jakarta.annotation.PostConstruct; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PreDestroy; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + + +/** + * Window Aggregator Responsible for: + * 1. Managing window data structure for all active windows + * 2. Receiving matching logs from Workers + * 3. Receiving watermarks from TimeService + * 4. Sending closed windows to AlarmEvaluator + */ +@Component +@Slf4j +public class WindowAggregator implements TimeService.WatermarkListener, Runnable { + + private static final long DEFAULT_WINDOW_SIZE_MS = 1 * 60 * 1000; // 1 minutes + + private final AlarmEvaluator alarmEvaluator; + private final BlockingQueue<MatchingLogEvent> eventQueue = new LinkedBlockingQueue<>(); + private final Map<WindowKey, WindowData> activeWindows = new HashMap<>(); + private final Object windowLock = new Object(); + + private ExecutorService aggregatorExecutor; + + public WindowAggregator(AlarmEvaluator alarmEvaluator) { + this.alarmEvaluator = alarmEvaluator; + } + + public void addMatchingLog(MatchingLogEvent event) { + try { + eventQueue.put(event); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("Interrupted while adding matching log to aggregator"); + } + } + + @Override + public void onWatermark(TimeService.Watermark watermark) { + List<WindowData> closedWindows; + + synchronized (windowLock) { + closedWindows = new ArrayList<>(); + + // Find windows that should be closed based on watermark + Iterator<Map.Entry<WindowKey, WindowData>> iterator = activeWindows.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry<WindowKey, WindowData> entry = iterator.next(); + WindowData windowData = entry.getValue(); + + // Close window if its end time <= watermark timestamp + if (windowData.getEndTime() <= watermark.getTimestamp()) { + closedWindows.add(windowData); + iterator.remove(); + + log.debug("Closing window: {} with {} matching logs", + entry.getKey(), windowData.getMatchingLogs().size()); + } + } + } + + for (WindowData windowData : closedWindows) { + alarmEvaluator.sendAndProcessWindowData(windowData); + } + } + + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + MatchingLogEvent event = eventQueue.take(); + processMatchingLogEvent(event); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (Exception e) { + log.error("Error processing matching log event: {}", e.getMessage(), e); + } + } + } + + private void processMatchingLogEvent(MatchingLogEvent event) { + + // Determine window size from alert define (if specified) or use default + long windowSizeMs = getWindowSize(event.getAlertDefine()); + + // Calculate window boundaries + long eventTime = event.getEventTimestamp(); + long windowStart = (eventTime / windowSizeMs) * windowSizeMs; + long windowEnd = windowStart + windowSizeMs; + + // Create window key + WindowKey windowKey = new WindowKey( + event.getAlertDefine().getId(), + windowStart, + windowEnd + ); + synchronized (windowLock) { + // Get or create window data + WindowData windowData = activeWindows.computeIfAbsent(windowKey, + key -> new WindowData(key, event.getAlertDefine())); + // Add matching log to window + windowData.addMatchingLog(event); + log.debug("Added matching log to window: {} (total logs: {})", + windowKey, windowData.getMatchingLogs().size()); + } + } + + private long getWindowSize(AlertDefine alertDefine) { + // Check if alert define has custom window size configuration + if (alertDefine.getPeriod() != null) { + return alertDefine.getPeriod() * 1000; // Convert seconds to milliseconds + } + log.info("Using default window size of {} ms for alert define: {}", + DEFAULT_WINDOW_SIZE_MS, alertDefine.getName()); + return DEFAULT_WINDOW_SIZE_MS; + } + + @PostConstruct + public void start() { + // Create internal executor + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("WindowAggregator executor has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("window-aggregator-%d") + .build(); + + aggregatorExecutor = Executors.newSingleThreadExecutor(threadFactory); + + // Submit aggregation task + aggregatorExecutor.submit(this); + + log.info("WindowAggregator started"); + } + + @PreDestroy + public void stop() { + if (aggregatorExecutor != null && !aggregatorExecutor.isShutdown()) { + log.info("Shutting down WindowAggregator executor..."); + aggregatorExecutor.shutdown(); + try { + if (!aggregatorExecutor.awaitTermination(10, TimeUnit.SECONDS)) { + log.warn("WindowAggregator executor did not terminate within 10 seconds, forcing shutdown"); + aggregatorExecutor.shutdownNow(); + if (!aggregatorExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + log.error("WindowAggregator executor did not terminate"); + } + } + } catch (InterruptedException e) { + log.warn("Interrupted while waiting for WindowAggregator executor to terminate"); + aggregatorExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + log.info("WindowAggregator stopped"); + } + + /** + * Window key for identifying unique windows + */ + @Data + @Getter + @AllArgsConstructor + public static class WindowKey { + private final long alertDefineId; + private final long startTime; + private final long endTime; + } + + /** + * Window data container + */ + public static class WindowData { + @Getter + private final WindowKey windowKey; + @Getter + private final AlertDefine alertDefine; + private final List<MatchingLogEvent> matchingLogs = new ArrayList<>(); + @Getter + private final long createdTime; + + public WindowData(WindowKey windowKey, AlertDefine alertDefine) { + this.windowKey = windowKey; + this.alertDefine = alertDefine; + this.createdTime = System.currentTimeMillis(); + } + + public void addMatchingLog(MatchingLogEvent event) { + matchingLogs.add(event); + } + + public List<MatchingLogEvent> getMatchingLogs() { + return new ArrayList<>(matchingLogs); + } + + /** + * Get start time of the window + * @return start time + */ + public long getStartTime() { return windowKey.getStartTime(); } + + /** + * Get end time of the window + * @return end time + */ + public long getEndTime() { return windowKey.getEndTime(); } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/config/AlertSseManager.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/config/AlertSseManager.java new file mode 100644 index 0000000..e3d2456 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/config/AlertSseManager.java @@ -0,0 +1,80 @@ +/* + * 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.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * SSE manager for alert + */ +@Slf4j +@Component +public class AlertSseManager { + private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>(); + + public SseEmitter createEmitter(Long clientId) { + SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); + emitter.onCompletion(() -> removeEmitter(clientId)); + emitter.onTimeout(() -> removeEmitter(clientId)); + emitter.onError((ex) -> removeEmitter(clientId)); + emitters.put(clientId, emitter); + return emitter; + } + + @Async + public void broadcast(String data) { + emitters.forEach((clientId, emitter) -> { + try { + emitter.send(SseEmitter.event() + .id(String.valueOf(System.currentTimeMillis())) + .name("ALERT_EVENT") + .data(data)); + } catch (IOException | IllegalStateException e) { + tryCompleteAndClean(clientId, emitter); + } catch (Exception exception) { + log.error("Failed to broadcast alert data to client: {}", exception.getMessage()); + tryCompleteAndClean(clientId, emitter); + } + }); + } + + private void tryCompleteAndClean(Long clientId, SseEmitter emitter) { + try { + Optional.ofNullable(emitter).ifPresent(ResponseBodyEmitter::complete); + } catch (Throwable e) { + log.debug("Failed to complete emitter for client {}: {}", clientId, e.getMessage()); + } + // execute clear + removeEmitter(clientId); + } + + private void removeEmitter(Long clientId) { + emitters.remove(clientId); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/config/AlerterAutoConfiguration.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/config/AlerterAutoConfiguration.java new file mode 100644 index 0000000..f75e01a --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/config/AlerterAutoConfiguration.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.config; + +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.ComponentScan; + +/** + * Alert auto configuration. + */ + +@AutoConfiguration +@ComponentScan(basePackages = ConfigConstants.PkgConstant.PKG + + SignConstants.DOT + + ConfigConstants.FunctionModuleConstants.ALERT) +public class AlerterAutoConfiguration { +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java new file mode 100644 index 0000000..04b9de2 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java @@ -0,0 +1,127 @@ +/* + * 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.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.alert.service.DataSourceService; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.support.exception.AlertExpressionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE; +import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +/** + * Alarm definition management API + */ +@Tag(name = "Alert Define API") +@RestController +@RequestMapping(path = "/api/alert/define", produces = {APPLICATION_JSON_VALUE}) +public class AlertDefineController { + + @Autowired + private AlertDefineService alertDefineService; + + @Autowired + private DataSourceService dataSourceService; + + @PostMapping + @Operation(summary = "New Alarm Definition", description = "Added an alarm definition") + public ResponseEntity<Message<Void>> addNewAlertDefine(@Valid @RequestBody AlertDefine alertDefine) { + // Verify request data + alertDefineService.validate(alertDefine, false); + alertDefineService.addAlertDefine(alertDefine); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping + @Operation(summary = "Modifying an Alarm Definition", description = "Modify an existing alarm definition") + public ResponseEntity<Message<Void>> modifyAlertDefine(@Valid @RequestBody AlertDefine alertDefine) { + // Verify request data + alertDefineService.validate(alertDefine, true); + alertDefineService.modifyAlertDefine(alertDefine); + return ResponseEntity.ok(Message.success("Modify success")); + } + + @GetMapping(path = "/{id}") + @Operation(summary = "Querying Alarm Definitions", + description = "You can obtain alarm definition information based on the alarm definition ID") + public ResponseEntity<Message<AlertDefine>> getAlertDefine( + @Parameter(description = "Alarm Definition ID", example = "6565463543") @PathVariable("id") long id) { + // Obtaining Monitoring Information + AlertDefine alertDefine = alertDefineService.getAlertDefine(id); + + return Objects.isNull(alertDefine) + ? ResponseEntity.ok(Message.fail(MONITOR_NOT_EXIST_CODE, "AlertDefine not exist.")) + : ResponseEntity.ok(Message.success(alertDefine)); + } + + @DeleteMapping(path = "/{id}") + @Operation(summary = "Deleting an Alarm Definition", + description = "If the alarm definition does not exist, the alarm is deleted successfully") + public ResponseEntity<Message<Void>> deleteAlertDefine( + @Parameter(description = "Alarm Definition ID", example = "6565463543") @PathVariable("id") long id) { + // If the alarm definition does not exist or is deleted successfully, the deletion succeeds + alertDefineService.deleteAlertDefine(id); + return ResponseEntity.ok(Message.success("Delete success")); + } + + @GetMapping(path = "/preview/{datasource}") + @Operation(summary = "Alarm definition expression preview", + description = "If the expression is formal, then the result of the query will be returned, otherwise it will respond with an error") + public ResponseEntity<Message<List<Map<String, Object>>>> getDefinePreview( + @Parameter(description = "Data Source Type", example = "promql") @PathVariable("datasource") String datasource, + @Parameter(description = "alert threshold type:realtime,periodic") @RequestParam String type, + @Parameter(description = "alert threshold expression") @RequestParam String expr) { + try { + return ResponseEntity.ok(Message.successWithData(alertDefineService.getDefinePreview(datasource, type, expr))); + } catch (AlertExpressionException ae) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Message.fail(FAIL_CODE, ae.getMessage())); + } + } + + @GetMapping(path = "/datasource/status") + @Operation(summary = "Get available datasource executors status", + description = "Get status of available datasource executors for periodic alerts") + public ResponseEntity<Message<Map<String, Object>>> getDatasourceStatus() { + Map<String, Object> status = dataSourceService.getAvailableExecutors(); + return ResponseEntity.ok(Message.successWithData(status)); + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefinesController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefinesController.java new file mode 100644 index 0000000..4eb8fe2 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefinesController.java @@ -0,0 +1,93 @@ +/* + * 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.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +/** + * Define the batch API for alarms + */ +@Tag(name = "Alert Define Batch API") +@RestController +@RequestMapping(path = "/api/alert/defines", produces = {APPLICATION_JSON_VALUE}) +public class AlertDefinesController { + + @Autowired + private AlertDefineService alertDefineService; + + @GetMapping + @Operation(summary = "Example Query the alarm definition list", + description = "You can obtain the list of alarm definitions by querying filter items") + public ResponseEntity<Message<Page<AlertDefine>>> getAlertDefines( + @Parameter(description = "Alarm Definition ID", example = "6565463543") @RequestParam(required = false) List<Long> ids, + @Parameter(description = "Search-Target Expr Template", example = "x") @RequestParam(required = false) String search, + @Parameter(description = "Sort field, default id", example = "id") @RequestParam(defaultValue = "id") String sort, + @Parameter(description = "Sort mode: asc: ascending, desc: descending", example = "desc") @RequestParam(defaultValue = "desc") String order, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pages", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + Page<AlertDefine> alertDefinePage = alertDefineService.getAlertDefines(ids, search, sort, order, pageIndex, pageSize); + return ResponseEntity.ok(Message.success(alertDefinePage)); + } + + @DeleteMapping + @Operation(summary = "Delete alarm definitions in batches", + description = "Delete alarm definitions in batches based on the alarm definition ID list") + public ResponseEntity<Message<Void>> deleteAlertDefines( + @Parameter(description = "Alarm Definition IDs", example = "6565463543") @RequestParam(required = false) List<Long> ids + ) { + if (ids != null && !ids.isEmpty()) { + alertDefineService.deleteAlertDefines(new HashSet<>(ids)); + } + return ResponseEntity.ok(Message.success()); + } + + @GetMapping("/export") + @Operation(summary = "export alertDefine config", description = "export alarm definition configuration") + public void export( + @Parameter(description = "AlertDefine ID List", example = "656937901") @RequestParam List<Long> ids, + @Parameter(description = "Export Type:JSON,EXCEL,YAML") @RequestParam(defaultValue = "JSON") String type, + HttpServletResponse res) throws Exception { + alertDefineService.export(ids, type, res); + } + + @PostMapping("/import") + @Operation(summary = "import alertDefine config", description = "import alarm definition configuration") + public ResponseEntity<Message<Void>> importDefines(MultipartFile file) throws Exception { + alertDefineService.importConfig(file); + return ResponseEntity.ok(Message.success("Import success")); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertGroupConvergeController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertGroupConvergeController.java new file mode 100644 index 0000000..d6a3380 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertGroupConvergeController.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.Objects; +import org.apache.hertzbeat.alert.service.AlertGroupConvergeService; +import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Alarm Converge management API + */ +@Tag(name = "Alert Converge API") +@RestController +@RequestMapping(path = "/api/alert/group", produces = {APPLICATION_JSON_VALUE}) +public class AlertGroupConvergeController { + + @Autowired + private AlertGroupConvergeService alertGroupConvergeService; + + @PostMapping + @Operation(summary = "New Alarm Group Converge", description = "Added an alarm Group Converge") + public ResponseEntity<Message<Void>> addNewAlertGroupConverge(@Valid @RequestBody AlertGroupConverge alertGroupConverge) { + alertGroupConvergeService.validate(alertGroupConverge, false); + alertGroupConvergeService.addAlertGroupConverge(alertGroupConverge); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping + @Operation(summary = "Modifying an Alarm Group Converge", description = "Modify an existing alarm Group Converge") + public ResponseEntity<Message<Void>> modifyAlertGroupConverge(@Valid @RequestBody AlertGroupConverge alertGroupConverge) { + alertGroupConvergeService.validate(alertGroupConverge, true); + alertGroupConvergeService.modifyAlertGroupConverge(alertGroupConverge); + return ResponseEntity.ok(Message.success("Modify success")); + } + + @GetMapping(path = "/{id}") + @Operation(summary = "Querying Alarm Group Converge", + description = "You can obtain alarm Group Converge information based on the alarm Converge ID") + public ResponseEntity<Message<AlertGroupConverge>> getAlertGroupConverge( + @Parameter(description = "Alarm Converge ID", example = "6565463543") @PathVariable("id") long id) { + AlertGroupConverge alertGroupConverge = alertGroupConvergeService.getAlertGroupConverge(id); + + return Objects.isNull(alertGroupConverge) + ? ResponseEntity.ok(Message.fail(MONITOR_NOT_EXIST_CODE, "Alert Group Converge not exist.")) + : ResponseEntity.ok(Message.success(alertGroupConverge)); + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertGroupConvergesController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertGroupConvergesController.java new file mode 100644 index 0000000..769a8c8 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertGroupConvergesController.java @@ -0,0 +1,75 @@ +/* + * 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.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.service.AlertGroupConvergeService; +import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Converge the batch API for alarms + */ +@Tag(name = "Alert Converge Batch API") +@RestController +@RequestMapping(path = "/api/alert/groups", produces = {APPLICATION_JSON_VALUE}) +public class AlertGroupConvergesController { + + @Autowired + private AlertGroupConvergeService alertGroupConvergeService; + + @GetMapping + @Operation(summary = "Query the alarm group converge list", + description = "You can obtain the list of alarm group group converge by querying filter items") + public ResponseEntity<Message<Page<AlertGroupConverge>>> getAlertGroupConverges( + @Parameter(description = "Alarm Converge ID", example = "6565463543") @RequestParam(required = false) List<Long> ids, + @Parameter(description = "Search Name", example = "x") @RequestParam(required = false) String search, + @Parameter(description = "Sort field, default id", example = "id") @RequestParam(defaultValue = "id") String sort, + @Parameter(description = "Sort mode: asc: ascending, desc: descending", example = "desc") @RequestParam(defaultValue = "desc") String order, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pages", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + Page<AlertGroupConverge> alertGroupConvergePage = alertGroupConvergeService.getAlertGroupConverges(ids, search, sort, order, pageIndex, pageSize); + return ResponseEntity.ok(Message.success(alertGroupConvergePage)); + } + + @DeleteMapping + @Operation(summary = "Delete alarm group converge in batches", + description = "Delete alarm group converge in batches based on the alarm group converge ID list") + public ResponseEntity<Message<Void>> deleteAlertDefines( + @Parameter(description = "Alarm Converge IDs", example = "6565463543") @RequestParam(required = false) List<Long> ids + ) { + if (ids != null && !ids.isEmpty()) { + alertGroupConvergeService.deleteAlertGroupConverges(new HashSet<>(ids)); + } + return ResponseEntity.ok(Message.success()); + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertInhibitController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertInhibitController.java new file mode 100644 index 0000000..012e73a --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertInhibitController.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.Objects; +import org.apache.hertzbeat.alert.service.AlertInhibitService; +import org.apache.hertzbeat.common.entity.alerter.AlertInhibit; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Alarm Inhibit management API + */ +@Tag(name = "Alert Inhibit API") +@RestController +@RequestMapping(path = "/api/alert/inhibit", produces = {APPLICATION_JSON_VALUE}) +public class AlertInhibitController { + + @Autowired + private AlertInhibitService alertInhibitService; + + @PostMapping + @Operation(summary = "New Alarm Inhibit", description = "Added an alarm Inhibit") + public ResponseEntity<Message<Void>> addNewAlertInhibit(@Valid @RequestBody AlertInhibit alertInhibit) { + alertInhibitService.validate(alertInhibit, false); + alertInhibitService.addAlertInhibit(alertInhibit); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping + @Operation(summary = "Modifying an Alarm Inhibit", description = "Modify an existing alarm Inhibit") + public ResponseEntity<Message<Void>> modifyAlertInhibit(@Valid @RequestBody AlertInhibit alertInhibit) { + alertInhibitService.validate(alertInhibit, true); + alertInhibitService.modifyAlertInhibit(alertInhibit); + return ResponseEntity.ok(Message.success("Modify success")); + } + + @GetMapping(path = "/{id}") + @Operation(summary = "Querying Alarm Inhibit", + description = "You can obtain alarm Inhibit information based on the alarm Inhibit ID") + public ResponseEntity<Message<AlertInhibit>> getAlertInhibit( + @Parameter(description = "Alarm Inhibit ID", example = "6565463543") @PathVariable("id") long id) { + AlertInhibit alertInhibit = alertInhibitService.getAlertInhibit(id); + + return Objects.isNull(alertInhibit) + ? ResponseEntity.ok(Message.fail(MONITOR_NOT_EXIST_CODE, "AlertInhibit not exist.")) + : ResponseEntity.ok(Message.success(alertInhibit)); + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertInhibitsController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertInhibitsController.java new file mode 100644 index 0000000..3b85048 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertInhibitsController.java @@ -0,0 +1,76 @@ +/* + * 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.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.service.AlertInhibitService; +import org.apache.hertzbeat.common.entity.alerter.AlertInhibit; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Inhibit the batch API for alarms + */ +@Tag(name = "Alert Inhibit Batch API") +@RestController +@RequestMapping(path = "/api/alert/inhibits", produces = {APPLICATION_JSON_VALUE}) +public class AlertInhibitsController { + + @Autowired + private AlertInhibitService alertInhibitService; + + @GetMapping + @Operation(summary = "Query the alarm inhibit list", + description = "You can obtain the list of alarm inhibit by querying filter items") + public ResponseEntity<Message<Page<AlertInhibit>>> getAlertInhibits( + @Parameter(description = "Alarm Inhibit ID", example = "6565463543") @RequestParam(required = false) List<Long> ids, + @Parameter(description = "Search Name", example = "x") @RequestParam(required = false) String search, + @Parameter(description = "Sort field, default id", example = "id") @RequestParam(defaultValue = "id") String sort, + @Parameter(description = "Sort mode: asc: ascending, desc: descending", example = "desc") @RequestParam(defaultValue = "desc") String order, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pages", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + Page<AlertInhibit> alertInhibitPage = alertInhibitService.getAlertInhibits(ids, search, sort, order, pageIndex, pageSize); + return ResponseEntity.ok(Message.success(alertInhibitPage)); + } + + @DeleteMapping + @Operation(summary = "Delete alarm inhibit in batches", + description = "Delete alarm inhibit in batches based on the alarm inhibit ID list") + public ResponseEntity<Message<Void>> deleteAlertDefines( + @Parameter(description = "Alarm Inhibit IDs", example = "6565463543") @RequestParam(required = false) List<Long> ids + ) { + if (ids != null && !ids.isEmpty()) { + alertInhibitService.deleteAlertInhibits(new HashSet<>(ids)); + } + Message<Void> message = Message.success(); + return ResponseEntity.ok(message); + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertReportController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertReportController.java new file mode 100644 index 0000000..cf81040 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertReportController.java @@ -0,0 +1,115 @@ +/* + * 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.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.service.ExternAlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Extern Alarm Manage API + */ +@Tag(name = "Extern Alarm Manage API") +@RestController +@Slf4j +public class AlertReportController { + + private final List<ExternAlertService> externAlertServiceList; + + public AlertReportController(List<ExternAlertService> externAlertServiceList) { + this.externAlertServiceList = externAlertServiceList; + } + + @PostMapping("/api/alerts/report/{source}") + @Operation(summary = "Api for receive external alarm information") + public ResponseEntity<Message<Void>> receiveExternAlert(@PathVariable(value = "source") String source, + @RequestBody String content) { + log.info("Receive extern alert from source: {}, content: {}", source, content); + if (!StringUtils.hasText(source)) { + source = "default"; + } + for (ExternAlertService externAlertService : externAlertServiceList) { + if (externAlertService.supportSource().equals(source)) { + try { + externAlertService.addExternAlert(content); + return ResponseEntity.ok(Message.success("Add extern alert success")); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Message.fail(CommonConstants.FAIL_CODE, + "Add extern alert failed: " + e.getMessage())); + } + } + } + log.warn("Not support extern alert from source: {}", source); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Message.fail(CommonConstants.FAIL_CODE, "Not support the " + source + " source alert")); + } + + @PostMapping("/api/alerts/report") + @Operation(summary = "Api for receive default external alarm information") + public ResponseEntity<Message<Void>> receiveDefaultExternAlert(@RequestBody String content) { + log.info("Receive default extern alert content: {}", content); + ExternAlertService externAlertService = externAlertServiceList.stream() + .filter(item -> "default".equals(item.supportSource())).findFirst().orElse(null); + if (externAlertService != null) { + try { + externAlertService.addExternAlert(content); + return ResponseEntity.ok(Message.success("Add extern alert success")); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Message.fail(CommonConstants.FAIL_CODE, + "Add extern alert failed: " + e.getMessage())); + } + } + log.error("Not support default extern alert"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Message.success("Not support the default source alert")); + } + + @PostMapping("/api/v2/alerts") + @Operation(summary = "Api for receive external alarm information") + public ResponseEntity<Message<Void>> receivePrometheusServerAlert(@RequestBody String content) { + log.info("Receive prometheus server alert, content: {}", content); + ExternAlertService externAlertService = externAlertServiceList.stream() + .filter(item -> "prometheus".equals(item.supportSource())).findFirst().orElse(null); + if (externAlertService != null) { + try { + externAlertService.addExternAlert(content); + return ResponseEntity.ok(Message.success("Add extern alert success")); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Message.fail(CommonConstants.FAIL_CODE, + "Add extern alert failed: " + e.getMessage())); + } + } + log.error("Not support prometheus extern alert"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Message.success("Not support the prometheus source alert")); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSilenceController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSilenceController.java new file mode 100644 index 0000000..6e6e1d7 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSilenceController.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.Objects; +import org.apache.hertzbeat.alert.service.AlertSilenceService; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Alarm Silence management API + */ +@Tag(name = "Alert Silence API") +@RestController +@RequestMapping(path = "/api/alert/silence", produces = {APPLICATION_JSON_VALUE}) +public class AlertSilenceController { + + @Autowired + private AlertSilenceService alertSilenceService; + + @PostMapping + @Operation(summary = "New Alarm Silence", description = "Added an alarm Silence") + public ResponseEntity<Message<Void>> addNewAlertSilence(@Valid @RequestBody AlertSilence alertSilence) { + alertSilenceService.validate(alertSilence, false); + alertSilenceService.addAlertSilence(alertSilence); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping + @Operation(summary = "Modifying an Alarm Silence", description = "Modify an existing alarm Silence") + public ResponseEntity<Message<Void>> modifyAlertSilence(@Valid @RequestBody AlertSilence alertSilence) { + alertSilenceService.validate(alertSilence, true); + alertSilenceService.modifyAlertSilence(alertSilence); + return ResponseEntity.ok(Message.success("Modify success")); + } + + @GetMapping(path = "/{id}") + @Operation(summary = "Querying Alarm Silence", + description = "You can obtain alarm Silence information based on the alarm Silence ID") + public ResponseEntity<Message<AlertSilence>> getAlertSilence( + @Parameter(description = "Alarm Silence ID", example = "6565463543") @PathVariable("id") long id) { + AlertSilence alertSilence = alertSilenceService.getAlertSilence(id); + + return Objects.isNull(alertSilence) + ? ResponseEntity.ok(Message.fail(MONITOR_NOT_EXIST_CODE, "AlertSilence not exist.")) + : ResponseEntity.ok(Message.success(alertSilence)); + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSilencesController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSilencesController.java new file mode 100644 index 0000000..1b209b1 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSilencesController.java @@ -0,0 +1,76 @@ +/* + * 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.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.service.AlertSilenceService; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Silence the batch API for alarms + */ +@Tag(name = "Alert Silence Batch API") +@RestController +@RequestMapping(path = "/api/alert/silences", produces = {APPLICATION_JSON_VALUE}) +public class AlertSilencesController { + + @Autowired + private AlertSilenceService alertSilenceService; + + @GetMapping + @Operation(summary = "Query the alarm silence list", + description = "You can obtain the list of alarm silence by querying filter items") + public ResponseEntity<Message<Page<AlertSilence>>> getAlertSilences( + @Parameter(description = "Alarm Silence ID", example = "6565463543") @RequestParam(required = false) List<Long> ids, + @Parameter(description = "Search Name", example = "x") @RequestParam(required = false) String search, + @Parameter(description = "Sort field, default id", example = "id") @RequestParam(defaultValue = "id") String sort, + @Parameter(description = "Sort mode: asc: ascending, desc: descending", example = "desc") @RequestParam(defaultValue = "desc") String order, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pages", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + Page<AlertSilence> alertSilencePage = alertSilenceService.getAlertSilences(ids, search, sort, order, pageIndex, pageSize); + return ResponseEntity.ok(Message.success(alertSilencePage)); + } + + @DeleteMapping + @Operation(summary = "Delete alarm silence in batches", + description = "Delete alarm silence in batches based on the alarm silence ID list") + public ResponseEntity<Message<Void>> deleteAlertDefines( + @Parameter(description = "Alarm Silence IDs", example = "6565463543") @RequestParam(required = false) List<Long> ids + ) { + if (ids != null && !ids.isEmpty()) { + alertSilenceService.deleteAlertSilences(new HashSet<>(ids)); + } + Message<Void> message = Message.success(); + return ResponseEntity.ok(message); + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSseController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSseController.java new file mode 100644 index 0000000..61f6f3c --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSseController.java @@ -0,0 +1,48 @@ +/* + * 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.controller; + +import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE; +import org.apache.hertzbeat.alert.config.AlertSseManager; +import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +/** + * SSE controller for alert + */ +@RestController +@RequestMapping(path = "/api/alert/sse", produces = {TEXT_EVENT_STREAM_VALUE}) +public class AlertSseController { + + private final AlertSseManager emitterManager; + + public AlertSseController(AlertSseManager emitterManager) { + this.emitterManager = emitterManager; + } + + @GetMapping(path = "/subscribe") + public SseEmitter subscribe() { + Long clientId = SnowFlakeIdGenerator.generateId(); + return emitterManager.createEmitter(clientId); + } +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java new file mode 100644 index 0000000..d3b4957 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java @@ -0,0 +1,110 @@ +/* + * 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.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.dto.AlertSummary; +import org.apache.hertzbeat.alert.service.AlertService; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Alarm Management API + */ +@Tag(name = "Alarm Manage Batch API") +@RestController +@RequestMapping(path = "/api/alerts", produces = {APPLICATION_JSON_VALUE}) +public class AlertsController { + + @Autowired + private AlertService alertService; + + @GetMapping + @Operation(summary = "Query Alarms") + public ResponseEntity<Message<Page<SingleAlert>>> getAlerts( + @Parameter(description = "Alarm Status", example = "resolved") @RequestParam(required = false) String status, + @Parameter(description = "Alarm content fuzzy query", example = "linux") @RequestParam(required = false) String search, + @Parameter(description = "Sort field, default id", example = "name") @RequestParam(defaultValue = "gmtUpdate") String sort, + @Parameter(description = "Sort Type", example = "desc") @RequestParam(defaultValue = "desc") String order, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pagination", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + Page<SingleAlert> alertPage = alertService.getSingleAlerts(status, search, sort, order, pageIndex, pageSize); + return ResponseEntity.ok(Message.success(alertPage)); + } + + @GetMapping("/group") + @Operation(summary = "Query Group Alarms") + public ResponseEntity<Message<Page<GroupAlert>>> getGroupAlerts( + @Parameter(description = "Alarm Status", example = "resolved") @RequestParam(required = false) String status, + @Parameter(description = "Alarm content fuzzy query", example = "linux") @RequestParam(required = false) String search, + @Parameter(description = "Sort field, default id", example = "name") @RequestParam(defaultValue = "gmtUpdate") String sort, + @Parameter(description = "Sort Type", example = "desc") @RequestParam(defaultValue = "desc") String order, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pagination", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + Page<GroupAlert> alertPage = alertService.getGroupAlerts(status, search, sort, order, pageIndex, pageSize); + return ResponseEntity.ok(Message.success(alertPage)); + } + + @DeleteMapping("/group") + @Operation(summary = "Delete group alarms in batches", description = "according to the alarm ID list to delete the alarm information in batches") + public ResponseEntity<Message<Void>> deleteAlerts( + @Parameter(description = "Alarm List ID", example = "6565463543") @RequestParam(required = false) List<Long> ids) { + if (ids != null && !ids.isEmpty()) { + alertService.deleteGroupAlerts(new HashSet<>(ids)); + } + Message<Void> message = Message.success(); + return ResponseEntity.ok(message); + } + + @PutMapping(path = "/group/status/{status}") + @Operation(summary = "Batch modify alarm status, set firing or resolved", description = "Batch modify alarm status, set firing or resolved") + public ResponseEntity<Message<Void>> applyAlertDefinesStatus( + @Parameter(description = "Alarm status value", example = "resolved") @PathVariable String status, + @Parameter(description = "Alarm List IDS", example = "6565463543") @RequestParam(required = false) List<Long> ids) { + if (ids != null && status != null && !ids.isEmpty()) { + alertService.editGroupAlertStatus(status, ids); + } + Message<Void> message = Message.success(); + return ResponseEntity.ok(message); + } + + @GetMapping(path = "/summary") + @Operation(summary = "Get alarm statistics", description = "Get alarm statistics information") + public ResponseEntity<Message<AlertSummary>> getAlertsSummary() { + AlertSummary alertSummary = alertService.getAlertsSummary(); + Message<AlertSummary> message = Message.success(alertSummary); + return ResponseEntity.ok(message); + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java new file mode 100644 index 0000000..7a34928 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.List; +import java.util.Optional; +import org.apache.hertzbeat.common.entity.dto.Message; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Message Notification Configuration API + */ +@Tag(name = "Notification Config API") +@RestController() +@RequestMapping(value = "/api/notice", produces = {APPLICATION_JSON_VALUE}) +public class NoticeConfigController { + + @Autowired + private NoticeConfigService noticeConfigService; + + @PostMapping(path = "/receiver") + @Operation(summary = "Add a recipient", description = "Add a recipient") + public ResponseEntity<Message<Void>> addNewNoticeReceiver(@Valid @RequestBody NoticeReceiver noticeReceiver) { + noticeConfigService.addReceiver(noticeReceiver); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping(path = "/receiver") + @Operation(summary = "Modify existing recipient information", description = "Modify existing recipient information") + public ResponseEntity<Message<Void>> editNoticeReceiver(@Valid @RequestBody NoticeReceiver noticeReceiver) { + noticeConfigService.editReceiver(noticeReceiver); + return ResponseEntity.ok(Message.success("Edit success")); + } + + @DeleteMapping(path = "/receiver/{id}") + @Operation(summary = "Delete existing recipient information", description = "Delete existing recipient information") + public ResponseEntity<Message<Void>> deleteNoticeReceiver( + @Parameter(description = "en: Recipient ID", example = "6565463543") @PathVariable("id") final Long receiverId) { + NoticeReceiver noticeReceiver = noticeConfigService.getReceiverById(receiverId); + if (noticeReceiver == null) { + return ResponseEntity.ok(Message.success("The relevant information of the recipient could not be found, please check whether the parameters are correct")); + } + noticeConfigService.deleteReceiver(receiverId); + return ResponseEntity.ok(Message.success("Delete success")); + } + + @GetMapping(path = "/receivers") + @Operation(summary = "Get a list of message notification recipients based on query filter items", + description = "Get a list of message notification recipients based on query filter items") + public ResponseEntity<Message<Page<NoticeReceiver>>> getReceivers( + @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))); + } + + @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())); + } + + @GetMapping(path = "/receiver/{id}") + @Operation(summary = "Get the recipient information based on the recipient ID", + description = "Get the recipient information based on the recipient ID") + public ResponseEntity<Message<NoticeReceiver>> getReceiverById( + @Parameter(description = "en: Recipient ID", example = "6565463543") @PathVariable("id") final Long receiverId) { + NoticeReceiver noticeReceiver = noticeConfigService.getReceiverById(receiverId); + 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)); + } + + @PostMapping(path = "/rule") + @Operation(summary = "Add a notification policy", description = "Add a notification policy") + public ResponseEntity<Message<Void>> addNewNoticeRule(@Valid @RequestBody NoticeRule noticeRule) { + noticeConfigService.addNoticeRule(noticeRule); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping(path = "/rule") + @Operation(summary = "Modify existing notification policy information", description = "Modify existing notification policy information") + public ResponseEntity<Message<Void>> editNoticeRule(@Valid @RequestBody NoticeRule noticeRule) { + noticeConfigService.editNoticeRule(noticeRule); + return ResponseEntity.ok(Message.success("Edit success")); + } + + @DeleteMapping(path = "/rule/{id}") + @Operation(summary = "Delete existing notification policy information", description = "Delete existing notification policy information") + public ResponseEntity<Message<Void>> deleteNoticeRule( + @Parameter(description = "en: Notification Policy ID", example = "6565463543") @PathVariable("id") final Long ruleId) { + // Returns success if it does not exist or if the deletion is successful + NoticeRule noticeRule = noticeConfigService.getNoticeRulesById(ruleId); + if (noticeRule == null) { + return ResponseEntity.ok(Message.success("The specified notification rule could not be queried, please check whether the parameters are correct")); + } + noticeConfigService.deleteNoticeRule(ruleId); + return ResponseEntity.ok(Message.success("Delete success")); + } + + @GetMapping(path = "/rules") + @Operation(summary = "Get a list of message notification policies based on query filter items", + description = "Get a list of message notification policies based on query filter items") + public ResponseEntity<Message<Page<NoticeRule>>> getRules( + @Parameter(description = "en: Recipient name", example = "rule1") @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.getNoticeRules(name, pageIndex, pageSize))); + } + + @GetMapping(path = "/rule/{id}") + @Operation(summary = "Get the notification policy information based on the policy ID", + description = "Get the notification policy information based on the policy ID") + public ResponseEntity<Message<NoticeRule>> getRuleById( + @Parameter(description = "en: Notification Policy ID", example = "6565463543") @PathVariable("id") final Long ruleId) { + NoticeRule noticeRule = noticeConfigService.getNoticeRulesById(ruleId); + if (noticeRule == null) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "The specified notification rule could not be queried, please check whether the parameters are correct or refresh the page")); + } + return ResponseEntity.ok(Message.success(noticeRule)); + } + + @PostMapping(path = "/template") + @Operation(summary = "Add a notification template", description = "Add a notification template") + public ResponseEntity<Message<Void>> addNewNoticeTemplate(@Valid @RequestBody NoticeTemplate noticeTemplate) { + noticeConfigService.addNoticeTemplate(noticeTemplate); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping(path = "/template") + @Operation(summary = "Modify existing notification template information", description = "Modify existing notification template information") + public ResponseEntity<Message<Void>> editNoticeTemplate(@Valid @RequestBody NoticeTemplate noticeTemplate) { + noticeConfigService.editNoticeTemplate(noticeTemplate); + return ResponseEntity.ok(Message.success("Edit success")); + } + + @DeleteMapping(path = "/template/{id}") + @Operation(summary = "Delete existing notification template information", description = "Delete existing notification template information") + public ResponseEntity<Message<Void>> deleteNoticeTemplate( + @Parameter(description = "en: Notification template ID", example = "6565463543") @PathVariable("id") final Long templateId) { + // Returns success if it does not exist or if the deletion is successful + Optional<NoticeTemplate> noticeTemplate = noticeConfigService.getNoticeTemplatesById(templateId); + if (noticeTemplate.isEmpty()) { + return ResponseEntity.ok(Message.success("The specified notification template could not be queried, please check whether the parameters are correct")); + } + noticeConfigService.deleteNoticeTemplate(templateId); + return ResponseEntity.ok(Message.success("Delete success")); + } + + @GetMapping(path = "/templates") + @Operation(summary = "Get a list of message notification templates based on query filter items", + description = "Get a list of message notification templates based on query filter items") + public ResponseEntity<Message<Page<NoticeTemplate>>> getTemplates( + @Parameter(description = "Template name,support fuzzy query", example = "rule1") @RequestParam(required = false) final String name, + @Parameter(description = "Whether it is a preset template", example = "true") @RequestParam(defaultValue = "true") final boolean preset, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") final int pageIndex, + @Parameter(description = "Number of list pages", example = "8") @RequestParam(defaultValue = "8") final int pageSize) { + Page<NoticeTemplate> templatePage = noticeConfigService.getNoticeTemplates(name, preset, pageIndex, pageSize); + return ResponseEntity.ok(Message.success(templatePage)); + } + + @GetMapping(path = "/templates/all") + @Operation(summary = "Get a list of all message notification templates", + description = "Get a list of all message notification templates") + public ResponseEntity<Message<List<NoticeTemplate>>> getAllTemplates() { + return ResponseEntity.ok(Message.success(noticeConfigService.getAllNoticeTemplates())); + } + + @GetMapping(path = "/template/{id}") + @Operation(summary = "Get the notification template information based on the template ID", + description = "Get the notification template information based on the template ID") + public ResponseEntity<Message<NoticeTemplate>> getTemplateById( + @Parameter(description = "en: Notification template ID", example = "6565463543") @PathVariable("id") final Long templateId) { + Optional<NoticeTemplate> noticeTemplate = noticeConfigService.getNoticeTemplatesById(templateId); + if (noticeTemplate.isEmpty()) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "The specified notification template could not be queried, please check whether the parameters are correct or refresh the page")); + } + return ResponseEntity.ok(Message.success(noticeTemplate.get())); + } + + @PostMapping(path = "/receiver/send-test-msg") + @Operation(summary = "Send test msg to receiver", description = "Send test msg to receiver") + public ResponseEntity<Message<Void>> sendTestMsg(@Valid @RequestBody NoticeReceiver noticeReceiver) { + boolean sendFlag = noticeConfigService.sendTestMsg(noticeReceiver); + if (sendFlag) { + return ResponseEntity.ok(Message.success()); + } + return ResponseEntity.ok(Message.fail(FAIL_CODE, "Notify service not available, please check config!")); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertCollectorDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertCollectorDao.java new file mode 100644 index 0000000..c76a06e --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertCollectorDao.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dao; + +import org.apache.hertzbeat.common.entity.manager.Collector; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * Alert Collector Dao + */ +public interface AlertCollectorDao extends JpaRepository<Collector, Long>, JpaSpecificationExecutor<Collector> { + + /** + * Query collector by name + * @param name collector name + * @return collector + */ + Collector findCollectorByName(String name); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDefineBindDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDefineBindDao.java new file mode 100644 index 0000000..76bb187 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDefineBindDao.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dao; + +import java.util.List; +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertDefineMonitorBind; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * AlertDefineBind database operations + */ +public interface AlertDefineBindDao extends JpaRepository<AlertDefineMonitorBind, Long>, JpaSpecificationExecutor<AlertDefineMonitorBind> { + + /** + * Delete the alarm definition and monitor association based on the alarm definition ID + * @param alertDefineId Alarm Definition ID + */ + void deleteAlertDefineBindsByAlertDefineIdEquals(Long alertDefineId); + + /** + * Deleting alarms based on monitoring IDs defines monitoring associations + * @param monitorId Monitor Id + */ + void deleteAlertDefineMonitorBindsByMonitorIdEquals(Long monitorId); + + /** + * Delete alarm definition monitoring association based on monitoring ID list + * @param monitorIds Monitoring ID List + */ + void deleteAlertDefineMonitorBindsByMonitorIdIn(Set<Long> monitorIds); + + /** + * Query monitoring related information based on alarm definition ID + * @param alertDefineId Alarm Definition ID + * @return Associated monitoring information + */ + List<AlertDefineMonitorBind> getAlertDefineBindsByAlertDefineIdEquals(Long alertDefineId); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDefineDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDefineDao.java new file mode 100644 index 0000000..51e256c --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDefineDao.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dao; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * AlertDefine Dao + */ +public interface AlertDefineDao extends JpaRepository<AlertDefine, Long>, JpaSpecificationExecutor<AlertDefine> { + + /** + * Delete alarm definitions based on the ID list + * @param alertDefineIds alarm define ids + */ + void deleteAlertDefinesByIdIn(Set<Long> alertDefineIds); + + /** + * Query the alarm define by type + * @param type alarm type + * @return alarm defines + */ + List<AlertDefine> findAlertDefinesByTypeAndEnableTrue(String type); + + /** + * Query the alarm define by name + * @param name alarm name + * @return alarm define + */ + Optional<AlertDefine> findAlertDefineByName(String name); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertGroupConvergeDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertGroupConvergeDao.java new file mode 100644 index 0000000..e74f12d --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertGroupConvergeDao.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dao; + +import java.util.List; +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; + +/** + * AlertConverge Dao + */ +public interface AlertGroupConvergeDao extends JpaRepository<AlertGroupConverge, Long>, JpaSpecificationExecutor<AlertGroupConverge> { + + /** + * Delete group alarm converge based on the ID list + * @param ids alert converge id list + */ + @Modifying + void deleteAlertGroupConvergesByIdIn(Set<Long> ids); + + /** + * Query the enable true group alarm converge list + * @return group alarm converge list + */ + List<AlertGroupConverge> findAlertGroupConvergesByEnableIsTrue(); + + /** + * Query group alarm converge list based on the name + * @param name alert converge name + * @return group alarm converge list + */ + List<AlertGroupConverge> findAlertGroupConvergesByName(String name); + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertInhibitDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertInhibitDao.java new file mode 100644 index 0000000..6a5355f --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertInhibitDao.java @@ -0,0 +1,45 @@ +/* + * 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.dao; + +import java.util.List; +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertInhibit; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; + +/** + * AlertInhibit Dao + */ +public interface AlertInhibitDao extends JpaRepository<AlertInhibit, Long>, JpaSpecificationExecutor<AlertInhibit> { + + /** + * Delete alarm inhibit based on the ID list + * + * @param ids alert inhibit id list + */ + @Modifying + void deleteAlertInhibitsByIdIn(Set<Long> ids); + + /** + * Query the enable true alarm inhibit list + * @return alarm inhibit list + */ + List<AlertInhibit> findAlertInhibitsByEnableIsTrue(); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertMonitorDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertMonitorDao.java new file mode 100644 index 0000000..938af35 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertMonitorDao.java @@ -0,0 +1,48 @@ +/* + * 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.dao; + +import java.util.List; +import java.util.Optional; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * Alert Monitor Dao + */ +public interface AlertMonitorDao extends JpaRepository<Monitor, Long>, JpaSpecificationExecutor<Monitor> { + + /** + * Query the monitoring status of a specified monitoring state + * @param status status value + * @return Monitor the list + */ + List<Monitor> findMonitorsByStatus(Byte status); + + + /** + * Query the monitoring + * @param id id + * @return monitor + */ + @Query("select monitor from Monitor monitor where monitor.id = :id") + Optional<Monitor> findMonitorById(@Param(value = "id") Long id); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertSilenceDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertSilenceDao.java new file mode 100644 index 0000000..4f29f23 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertSilenceDao.java @@ -0,0 +1,45 @@ +/* + * 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.dao; + +import java.util.List; +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; + +/** + * AlertSilence Dao + */ +public interface AlertSilenceDao extends JpaRepository<AlertSilence, Long>, JpaSpecificationExecutor<AlertSilence> { + + /** + * Delete alarm silence based on the ID list + * + * @param silenceIds alert silence id list + */ + @Modifying + void deleteAlertSilencesByIdIn(Set<Long> silenceIds); + + /** + * find enabled alert silence + * @return enabled alert silence list + */ + List<AlertSilence> findAlertSilencesByEnableTrue(); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/GroupAlertDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/GroupAlertDao.java new file mode 100644 index 0000000..4bb88a3 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/GroupAlertDao.java @@ -0,0 +1,63 @@ +/* + * 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.dao; + +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * Group Alert Database Operations + */ +public interface GroupAlertDao extends JpaRepository<GroupAlert, Long>, JpaSpecificationExecutor<GroupAlert> { + + /** + * Query alert group by groupKey + * @param groupKey group key identifier + * @return alert group + */ + GroupAlert findByGroupKey(String groupKey); + + /** + * Delete alerts based on ID list + * @param ids Alert ID List + */ + @Modifying + void deleteGroupAlertsByIdIn(HashSet<Long> ids); + + /** + * Updates the alarm status based on the alarm ID-status value + * @param status status value + * @param ids alarm ids + */ + @Modifying + @Query("update GroupAlert set status = :status where id in :ids") + void updateGroupAlertsStatus(@Param(value = "status") String status, @Param(value = "ids") List<Long> ids); + + /** + * find group alerts by id list + * @param ids ids + * @return group alerts + */ + List<GroupAlert> findGroupAlertsByIdIn(HashSet<Long> ids); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/NoticeReceiverDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/NoticeReceiverDao.java new file mode 100644 index 0000000..030b18a --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/NoticeReceiverDao.java @@ -0,0 +1,29 @@ +/* + * 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.dao; + +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * NoticeReceiver database operation + */ +public interface NoticeReceiverDao extends JpaRepository<NoticeReceiver, Long>, JpaSpecificationExecutor<NoticeReceiver> { + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/NoticeRuleDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/NoticeRuleDao.java new file mode 100644 index 0000000..f1bfac6 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/NoticeRuleDao.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dao; + +import java.util.List; +import org.apache.hertzbeat.common.entity.alerter.NoticeRule; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * NoticeRule database operations + */ +public interface NoticeRuleDao extends JpaRepository<NoticeRule, Long>, JpaSpecificationExecutor<NoticeRule> { + + /** + * Query all enabled notification policies + * @return notification policies + */ + List<NoticeRule> findNoticeRulesByEnableTrue(); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/NoticeTemplateDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/NoticeTemplateDao.java new file mode 100644 index 0000000..af9fb11 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/NoticeTemplateDao.java @@ -0,0 +1,29 @@ +/* + * 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.dao; + +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * Query all enabled notification policies + */ +public interface NoticeTemplateDao extends JpaRepository<NoticeTemplate, Long>, JpaSpecificationExecutor<NoticeTemplate> { + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/SingleAlertDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/SingleAlertDao.java new file mode 100644 index 0000000..d5256a3 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/SingleAlertDao.java @@ -0,0 +1,77 @@ +/* + * 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.dao; + +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * Single Alert Database Operations + */ +public interface SingleAlertDao extends JpaRepository<SingleAlert, Long>, JpaSpecificationExecutor<SingleAlert> { + + /** + * Query alert by fingerprint + * @param fingerprint alert fingerprint + * @return alert + */ + SingleAlert findByFingerprint(String fingerprint); + + /** + * Query alerts by fingerprint list + * @param fingerprints alert fingerprint list + * @return alerts + */ + List<SingleAlert> findSingleAlertsByFingerprintIn(List<String> fingerprints); + + /** + * Query alerts by status + * @param status status firing or resolved + * @return alerts + */ + List<SingleAlert> querySingleAlertsByStatus(String status); + + /** + * Delete alerts based on ID list + * @param ids Alert ID List + */ + @Modifying + void deleteSingleAlertsByIdIn(HashSet<Long> ids); + + /** + * Updates the alarm status based on the alarm ID-status value + * @param status status value + * @param ids alarm ids + */ + @Modifying + @Query("update SingleAlert set status = :status where id in :ids") + void updateSingleAlertsStatus(@Param(value = "status") String status, @Param(value = "ids") List<Long> ids); + + /** + * delete alerts by fingerprint list + * @param firingAlerts fingerprint list + */ + @Modifying + void deleteSingleAlertsByFingerprintIn(List<String> firingAlerts); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertDefineDTO.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertDefineDTO.java new file mode 100644 index 0000000..3ac0158 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertDefineDTO.java @@ -0,0 +1,54 @@ +/* + * 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.dto; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import cn.afterturn.easypoi.excel.annotation.ExcelTarget; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.Map; +import lombok.Data; + +/** + * Data transfer object for alert configuration + */ + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ExcelTarget(value = "AlertDefineDTO") +public class AlertDefineDTO { + @Excel(name = "Name") + private String name; + @Excel(name = "Type") + private String type; + @Excel(name = "Expr") + private String expr; + @Excel(name = "Period") + private Integer period; + @Excel(name = "Times") + private Integer times; + @Excel(name = "Labels") + private Map<String, String> labels; + @Excel(name = "Annotations") + private Map<String, String> annotations; + @Excel(name = "Template") + private String template; + @Excel(name = "Enable") + private Boolean enable; +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertManagerExternAlert.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertManagerExternAlert.java new file mode 100644 index 0000000..ccee487 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertManagerExternAlert.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dto; + +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * AlertManagerExternAlert + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AlertManagerExternAlert { + + private String groupKey; + + private String externalURL; + + private String version; + + private String status; + + private Map<String, String> groupLabels; + + private Map<String, String> commonLabels; + + private Map<String, String> commonAnnotations; + + private List<PrometheusExternAlert> alerts; +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertPriorityNum.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertPriorityNum.java new file mode 100644 index 0000000..8e85eff --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertPriorityNum.java @@ -0,0 +1,39 @@ +/* + * 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.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Number of monitoring level alarms + */ +@Data +@AllArgsConstructor +public class AlertPriorityNum { + + /** + * Alarm level + */ + private byte priority; + + /** + * Alarm count + */ + private long num; +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertSummary.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertSummary.java new file mode 100644 index 0000000..f75a680 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertSummary.java @@ -0,0 +1,59 @@ +/* + * 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.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * Alarm Statistics Information + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Alarm Statistics Information") +public class AlertSummary { + + @Schema(title = "Total number of alerts (including processed and unprocessed alerts)", + example = "134", accessMode = READ_ONLY) + private long total; + + @Schema(title = "Number of alerts handled", + example = "34", accessMode = READ_ONLY) + private long dealNum; + + @Schema(title = "Alarm handling rate", + example = "39.34", accessMode = READ_ONLY) + private float rate; + + @Schema(title = "Number of alarms whose alarm severity is warning alarms (referring to unhandled alarms)", + example = "43", accessMode = READ_ONLY) + private long priorityWarningNum; + + @Schema(title = "Number of alarms whose alarm severity is critical alarms (referring to unhandled alarms)", + example = "56", accessMode = READ_ONLY) + private long priorityCriticalNum; + + @Schema(title = "Number of alarms whose alarm severity is urgent alarms (referring to unhandled alarms)", + example = "23", accessMode = READ_ONLY) + private long priorityEmergencyNum; +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlibabaCloudSlsExternAlert.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlibabaCloudSlsExternAlert.java new file mode 100644 index 0000000..01a40e3 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlibabaCloudSlsExternAlert.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dto; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + + +/** + * Alibaba Cloud 'Simple Log Service(SLS)' alert content entity. + * + * @see <a href="https://help.aliyun.com/zh/sls/user-guide/variables-in-new-alert-templates"/> + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AlibabaCloudSlsExternAlert { + + /** + * The id of the instance on which the alarm was triggered + */ + @JsonProperty("alert_instance_id") + private String alertInstanceId; + + /** + * Alarm rule id, unique within project + */ + @JsonProperty("alert_id") + private String alertId; + + /** + * Alarm rule name + */ + @JsonProperty("alert_name") + private String alertName; + + /** + * Region + */ + private String region; + + /** + * Alarm rule belongs to Project + */ + private String project; + + /** + * Time of this evaluation + */ + @JsonProperty("alert_time") + private int alertTime; + + /** + * First trigger time + */ + @JsonProperty("fire_time") + private int fireTime; + + /** + * Alarm recovery time + * If the alarm status is firing, the value is 0. + * If the alarm state is resolved, the value is the specific recovery time. + */ + @JsonProperty("resolve_time") + private int resolveTime; + + /** + * Alarm status. + * firing: Triggers an alarm. + * resolved: Notification of resumption. + */ + private String status; + + /** + * The total number of entries in the data that triggered the alert, + * which may be more than 100, for example after a Cartesian product operation. + */ + @JsonProperty("fire_results_count") + private int fireResultsCount; + + /** + * Tag list + * Example: {"env":"test"} + */ + private Map<String, String> labels; + + /** + * Labeled lists + * Example: { "title": "Alarm title","desc": "Alarm desc" } + */ + private Map<String, String> annotations; + + /** + * Alarm severity. + * + * 10: Critical + * 8: High + * 6: Medium + * 4: Low + * 2: Report only + */ + private int severity; + + /** + * + */ + @JsonProperty("signin_url") + private String signinUrl; + + + public String getAnnotation(String key) { + if (null == this.annotations || this.annotations.isEmpty()) { + return "N/A"; + } + return this.annotations.get(key); + } + + /** + * Severity + */ + public enum Severity { + + CRITICAL(10, "Critical"), + + HIGH(8, "High"), + + MEDIUM(6, "Medium"), + + LOW(4, "Low"), + + REPORT_ONLY(2, "Report only"); + + private static final Map<Integer, Severity> STATUS_MAP; + + static { + STATUS_MAP = Arrays.stream(Severity.values()).collect(Collectors.toMap(Severity::getStatus, t -> t, (oldVal, newVal) -> newVal)); + } + + private final int status; + private final String alias; + + Severity(int status, String alias) { + this.status = status; + this.alias = alias; + } + + public static Optional<Severity> convert(int severity) { + return Optional.ofNullable(STATUS_MAP.get(severity)); + } + + public int getStatus() { + return status; + } + + public String getAlias() { + return alias; + } + } +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/ExportAlertDefineDTO.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/ExportAlertDefineDTO.java new file mode 100644 index 0000000..fb7f25f --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/ExportAlertDefineDTO.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dto; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import cn.afterturn.easypoi.excel.annotation.ExcelTarget; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +/** + * Export data transfer objects for alert configurations + */ + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@ExcelTarget(value = "ExportAlertDefineDTO") +public class ExportAlertDefineDTO { + + @Excel(name = "AlertDefine") + private AlertDefineDTO alertDefine; +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/HuaweiCloudExternAlert.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/HuaweiCloudExternAlert.java new file mode 100644 index 0000000..a82157e --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/HuaweiCloudExternAlert.java @@ -0,0 +1,237 @@ +/* + * 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.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Arrays; + +/** + * Huawei Cloud (CES) alert content entity. + * + * @see <a href="https://support.huaweicloud.com/usermanual-ces/ces_01_0218.html"/> + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class HuaweiCloudExternAlert { + + public static final String FIELD_MESSAGE = "message"; + public static final String FIELD_MESSAGE_ID = "message_id"; + public static final String FIELD_TIMESTAMP = "timestamp"; + public static final String FIELD_TOPIC_URN = "topic_urn"; + public static final String FIELD_TYPE = "type"; + public static final String FIELD_SUBJECT = "subject"; + public static final String FIELD_SUBSCRIBE_URL = "subscribe_url"; + + /** + * Signature information. + */ + private String signature; + + /** + * Subject + */ + private String subject; + + /** + * The unique identifier of a topic, indicating the topic to which the message belongs. + */ + @JsonProperty("topic_urn") + private String topicUrn; + + /** + * Message unique identifier. + */ + @JsonProperty("message_id") + private String messageId; + + /** + * Message + */ + private String message; + + /** + * message types, the message types are respectively: + * SubscriptionConfirmation、Notification、UnsubscribeConfirmation + */ + private String type; + + /** + * Subscription confirms the URL that needs to be accessed + */ + @JsonProperty("subscribe_url") + private String subscribeUrl; + + /** + * The certificate URL used for message signing, which does not require authentication and can be accessed directly. + */ + @JsonProperty("signing_cert_url") + private String signingCertUrl; + + /** + * The timestamp of when the message was first sent. + */ + private String timestamp; + + /** + * Alert message + */ + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class AlertMessage { + + private String version; + + private AlertData data; + } + + /** + * Alert data + */ + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class AlertData { + + /** + * Whether an alarm occurs. + * Note: Empty and false are both recovery notifications. + * Note: There are no recovery notifications for event types + */ + @JsonProperty("IsAlarm") + private Boolean alarm; + + /** + * Alarm time + */ + @JsonProperty("AlarmTime") + private String alarmTime; + + /** + * Resource ID + */ + @JsonProperty("ResourceId") + private String resourceId; + + /** + * The name of the metric + */ + @JsonProperty("MetricName") + private String metricName; + + /** + * Specifies the alarm severity, which can be Critical, Major, Minor, or Informational. + */ + @JsonProperty("AlarmLevel") + private String alarmLevel; + + /** + * Namespace + */ + @JsonProperty("Namespace") + private String namespace; + + /** + * Region + */ + @JsonProperty("Region") + private String region; + + /** + * Dimension name + */ + @JsonProperty("DimensionName") + private String dimensionName; + + /** + * Resource name + */ + @JsonProperty("ResourceName") + private String resourceName; + + /** + * Alarm record ID + */ + @JsonProperty("AlarmRecordID") + private String alarmRecordId; + + /** + * Current data + */ + @JsonProperty("CurrentData") + private String currentData; + + /** + * The comparison conditions for the alarm thresholds can be >, =, <, >=, <=. + */ + @JsonProperty("ComparisonOperator") + private String comparisonOperator; + + /** + * Alarm value + */ + @JsonProperty("Value") + private String value; + + /** + * Number of consecutive occurrences of triggered alarms + */ + @JsonProperty("Count") + private int count; + + } + + /** + * Huawei cloud alert type + */ + public enum AlertType { + + SUBSCRIPTION("SubscriptionConfirmation"), + + UNSUBSCRIBE("UnsubscribeConfirmation"), + + NOTIFICATION("Notification"); + + private final String type; + + AlertType(String type) { + this.type = type; + } + + public static boolean valid(String type) { + if (null == type || type.isEmpty()) { + return false; + } + return Arrays.stream(AlertType.values()).anyMatch(alertType -> alertType.getType().equals(type)); + } + + public String getType() { + return type; + } + } + +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/PrometheusExternAlert.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/PrometheusExternAlert.java new file mode 100644 index 0000000..83c61ab --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/PrometheusExternAlert.java @@ -0,0 +1,48 @@ +/* + * 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.dto; + +import java.time.Instant; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * Prometheus Alert Content Entity + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PrometheusExternAlert { + + private Map<String, String> labels; + + private Map<String, String> annotations; + + private String status; + + private Instant startsAt; + + private Instant endsAt; + + private String generatorURL; +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/SkyWalkingExternAlert.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/SkyWalkingExternAlert.java new file mode 100644 index 0000000..4bf2b37 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/SkyWalkingExternAlert.java @@ -0,0 +1,66 @@ +/* + * 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.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + + +/** + * SkyWalking Alert Content Entity + * @see <a href="https://skywalking.apache.org/docs/main/latest/en/setup/backend/backend-alarm/">backend-alarm</a> + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SkyWalkingExternAlert { + + private Long scopeId; + + private String scope; + + private String name; + + private String id0; + + private String id1; + + private String ruleName; + + private String alarmMessage; + + private Long startTime; + + private List<Tag> tags; + + /** + * SkyWalking Tag Entity + */ + @Data + public static class Tag { + + private String key; + + private String value; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/TencentCloudExternAlert.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/TencentCloudExternAlert.java new file mode 100644 index 0000000..e58e4a0 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/TencentCloudExternAlert.java @@ -0,0 +1,128 @@ +/* + * 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.dto; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * + * Tencent cloud alarm entity class + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class TencentCloudExternAlert { + @JsonProperty("sessionID") + private String sessionId; + private String alarmStatus; + private String alarmType; + private AlarmObjInfo alarmObjInfo; + private AlarmPolicyInfo alarmPolicyInfo; + private String firstOccurTime; + private int durationTime; + private String recoverTime; + + /** + * Alarm Object information + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class AlarmObjInfo { + private String region; + private String namespace; + @JsonProperty("appID") + private String appId; + private String uin; + private Dimensions dimensions; + } + + /** + * Uniform Resource ID information + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Dimensions { + @JsonProperty("unInstanceID") + private String unInstanceId; + @JsonProperty("objID") + private String objId; + } + + /** + * Alarm policy information + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class AlarmPolicyInfo { + @JsonProperty("policyID") + private String policyId; + private String policyType; + private String policyName; + @JsonProperty("policyTypeCName") + private String policyTypeCname; + private Conditions conditions; + } + + /** + * Parameters of indicator alarms + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Conditions { + // alarm metrics parameters + private String metricName; + private String metricShowName; + private String calcType; + private String calcValue; + private String calcUnit; + private String currentValue; + private String historyValue; + private String unit; + private String period; + private String periodNum; + private String alarmNotifyType; + private long alarmNotifyPeriod; + + // alarm event parameters + private String productName; + private String productShowName; + private String eventName; + private String eventShowName; + } + + /** + * Transaction alarm + */ + public static final String EVENT = "event"; + + /** + * Indicator alarm + */ + public static final String METRIC = "metric"; + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/UptimeKumaExternAlert.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/UptimeKumaExternAlert.java new file mode 100644 index 0000000..c11a5c5 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/UptimeKumaExternAlert.java @@ -0,0 +1,133 @@ +/* + * 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.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.List; +import java.util.Map; + +/** + * uptime-kuma alert content entity + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class UptimeKumaExternAlert { + + private Heartbeat heartbeat; + + private Monitor monitor; + + private String msg; + + /** + * Monitor information + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Heartbeat { + @JsonProperty("monitorID") + private int monitorId; + private int status; + private String time; + private String msg; + private boolean important; + private int duration; + private String timezone; + private String timezoneOffset; + private String localDateTime; + } + + /** + * Monitor information + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Monitor { + private int id; + private String name; + private String description; + private String pathName; + private String parent; + @JsonProperty("childrenIDs") + private List<String> childrenIds; + private String url; + private String method; + private String hostname; + private String port; + @JsonProperty("maxretries") + private int maxRetries; + private int weight; + private boolean active; + private boolean forceInactive; + private String type; + private int timeout; + private int interval; + private int retryInterval; + private int resendInterval; + private String keyword; + private boolean invertKeyword; + private boolean expiryNotification; + private boolean ignoreTls; + private boolean upsideDown; + private int packetSize; + @JsonProperty("maxredirects") + private int maxRedirects; + @JsonProperty("accepted_statuscodes") + private List<String> acceptedStatusCodes; + private String dnsResolveType; + private String dnsResolveServer; + private String dnsLastResult; + private String dockerContainer; + private String dockerHost; + private String proxyId; + private Map<String, Object> notificationIDList; + private List<String> tags; + private boolean maintenance; + private String mqttTopic; + private String mqttSuccessMessage; + private String databaseQuery; + private String authMethod; + private String grpcUrl; + private String grpcProtobuf; + private String grpcMethod; + private String grpcServiceName; + private boolean grpcEnableTls; + private String radiusCalledStationId; + private String radiusCallingStationId; + private String game; + private boolean gamedigGivenPortOnly; + private String httpBodyEncoding; + private String jsonPath; + private String expectedValue; + private String kafkaProducerTopic; + private List<String> kafkaProducerBrokers; + private boolean kafkaProducerSsl; + private boolean kafkaProducerAllowAutoTopicCreation; + private String kafkaProducerMessage; + private String screenshot; + private boolean includeSensitiveData; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/VolcEngineExternEventAlert.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/VolcEngineExternEventAlert.java new file mode 100644 index 0000000..05d4de5 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/VolcEngineExternEventAlert.java @@ -0,0 +1,121 @@ +/* + * 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.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + + +/** + * volcengine event alert entity class + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class VolcEngineExternEventAlert { + @JsonProperty("Type") + private String type; + + @JsonProperty("AccountId") + private String accountId; + + @JsonProperty("Source") + private String source; + + @JsonProperty("ProjectName") + private String projectName; + + @JsonProperty("Id") + private String id; + + @JsonProperty("EventType") + private String eventType; + + @JsonProperty("DescriptionCN") + private String descriptionCn; + + @JsonProperty("HappenedAt") + private Long happenedAt; + + @JsonProperty("Region") + private String region; + + @JsonProperty("Details") + private EventDetails details; + + @JsonProperty("Rules") + private List<EventRule> rules; + + /** + * event details + */ + @Data + public static class EventDetails { + @JsonProperty("specversion") + private String specVersion; + + @JsonProperty("id") + private String id; + + @JsonProperty("source") + private String source; + + @JsonProperty("type") + private String type; + + @JsonProperty("volcpublishtime") + private String volcPublishTime; + + @JsonProperty("volcregion") + private String volcRegion; + + @JsonProperty("volcaccountid") + private String volcAccountId; + + @JsonProperty("volceventbusname") + private String volcEventBusName; + + @JsonProperty("volcresourcename") + private String volcResourceName; + + @JsonProperty("subject") + private String subject; + + @JsonProperty("time") + private String time; + } + + /** + * event rule + */ + @Data + public static class EventRule { + @JsonProperty("RuleId") + private String ruleId; + + @JsonProperty("RuleName") + private String ruleName; + + @JsonProperty("Level") + private String level; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/VolcEngineExternMetricAlert.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/VolcEngineExternMetricAlert.java new file mode 100644 index 0000000..ae805b0 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/VolcEngineExternMetricAlert.java @@ -0,0 +1,203 @@ +/* + * 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.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Volcengine alarm entity class + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VolcEngineExternMetricAlert { + + @JsonProperty("Type") + private String type; + + @JsonProperty("AccountId") + private String accountId; + + @JsonProperty("RuleName") + private String ruleName; + + @JsonProperty("RuleId") + private String ruleId; + + @JsonProperty("Namespace") + private String namespace; + + @JsonProperty("SubNamespace") + private String subNamespace; + + @JsonProperty("Level") + private String level; + + @JsonProperty("HappenedAt") + private String happenedAt; + + @JsonProperty("RuleCondition") + private String ruleCondition; + + @JsonProperty("Resources") + private List<Resource> resources; + + @JsonProperty("RecoveredResources") + private List<Resource> recoveredResources; + + @JsonProperty("NoDataResources") + private List<NoDataResource> noDataResources; + + @JsonProperty("NoDataRecoveredResources") + private List<NoDataRecoveredResource> noDataRecoveredResources; + + /** + * alert resource entity class + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class Resource { + @JsonProperty("Id") + private String id; + + @JsonProperty("Name") + private String name; + + @JsonProperty("Region") + private String region; + + @JsonProperty("FirstAlertTime") + private Long firstAlertTime; + + @JsonProperty("LastAlertTime") + private Long lastAlertTime; + + @JsonProperty("Metrics") + private List<Metric> metrics; + + @JsonProperty("AlertGroupId") + private String alertGroupId; + + @JsonProperty("Dimensions") + private List<Dimension> dimensions; + } + + /** + * alert no data resource entity class + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class NoDataResource extends Resource { + @JsonProperty("NoDataMetrics") + private List<Metric> noDataMetrics; + } + + /** + * alert no data recovered resource entity class + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class NoDataRecoveredResource extends Resource { + @JsonProperty("DroppedMetrics") + private List<DroppedMetric> droppedMetrics; + } + + /** + * alert metric entity class + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Metric { + @JsonProperty("Name") + private String name; + + @JsonProperty("Unit") + private String unit; + + @JsonProperty("Threshold") + private Double threshold; + + @JsonProperty("CurrentValue") + private Object currentValue; + + @JsonProperty("Description") + private String description; + + @JsonProperty("Warning") + private Boolean warning; + } + + /** + * alert dropped metric entity class + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class DroppedMetric extends Metric { + @JsonProperty("Reason") + private String reason; + } + + /** + * resource dimension entity class + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Dimension { + @JsonProperty("Name") + private String name; + + @JsonProperty("NameCN") + private String nameCn; + + @JsonProperty("Value") + private String value; + + @JsonProperty("Description") + private String description; + } + + + public static final String ALERT_TYPE_METRIC = "Metric"; + public static final String ALERT_TYPE_EVENT = "Event"; + public static final String ALERT_TYPE_METRIC_RECOVERED = "MetricRecovered"; + public static final String ALERT_TYPE_METRICS_NODATA = "MetricsNoData"; + public static final String ALERT_TYPE_NO_DATA_RECOVERED = "NoDataRecovered"; + +} + + + + diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionBaseVisitor.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionBaseVisitor.java new file mode 100644 index 0000000..d24fac8 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionBaseVisitor.java @@ -0,0 +1,462 @@ +/* + * 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. + */ + +// Generated from AlertExpression.g4 by ANTLR 4.13.2 + +package org.apache.hertzbeat.alert.expr; + +import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor; + +/** + * This class provides an empty implementation of {@link AlertExpressionVisitor}, + * which can be extended to create a visitor which only needs to handle a subset + * of the available methods. + * + * @param <T> The return type of the visit operation. Use {@link Void} for + * operations with no return type. + */ +@SuppressWarnings("CheckReturnValue") +public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements AlertExpressionVisitor<T> { + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitExpression(AlertExpressionParser.ExpressionContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitAndExpr(AlertExpressionParser.AndExprContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitParenExpr(AlertExpressionParser.ParenExprContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitOrExpr(AlertExpressionParser.OrExprContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitParameterList(AlertExpressionParser.ParameterListContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitParameter(AlertExpressionParser.ParameterContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitNumber(AlertExpressionParser.NumberContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitString(AlertExpressionParser.StringContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitDuration(AlertExpressionParser.DurationContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitSelectField(AlertExpressionParser.SelectFieldContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitGroupByList(AlertExpressionParser.GroupByListContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitOrderByList(AlertExpressionParser.OrderByListContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitRelList(AlertExpressionParser.RelListContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitRelation(AlertExpressionParser.RelationContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitConditionList(AlertExpressionParser.ConditionListContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitCompOp(AlertExpressionParser.CompOpContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitCondition(AlertExpressionParser.ConditionContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitPromql(AlertExpressionParser.PromqlContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitLabelList(AlertExpressionParser.LabelListContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx) { + return visitChildren(ctx); + } + + /** + * {@inheritDoc} + * + * <p>The default implementation returns the result of calling + * {@link #visitChildren} on {@code ctx}.</p> + */ + @Override + public T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx) { + return visitChildren(ctx); + } +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionEvalVisitor.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionEvalVisitor.java new file mode 100644 index 0000000..bc84566 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionEvalVisitor.java @@ -0,0 +1,322 @@ +/* + * 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.expr; + +import org.antlr.v4.runtime.CommonTokenStream; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.hertzbeat.common.support.exception.ExpressionVisitorException; +import org.apache.hertzbeat.warehouse.db.QueryExecutor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Alert expression visitor implement + */ +public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<Map<String, Object>>> { + + private static final String SCALAR = "__scalar__"; + private static final String NAME = "__name__"; + private static final String VALUE = "__value__"; + private static final String TIMESTAMP = "__timestamp__"; + + private final QueryExecutor executor; + private final CommonTokenStream tokens; + + public AlertExpressionEvalVisitor(QueryExecutor executor, CommonTokenStream tokens) { + this.executor = executor; + this.tokens = tokens; + } + + @Override + public List<Map<String, Object>> visitExpression(AlertExpressionParser.ExpressionContext ctx) { + return visit(ctx.expr()); + } + + @Override + public List<Map<String, Object>> visitParenExpr(AlertExpressionParser.ParenExprContext ctx) { + return visit(ctx.expr()); + } + + @Override + public List<Map<String, Object>> visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) { + List<Map<String, Object>> leftResult = visit(ctx.left); + List<Map<String, Object>> rightResult = visit(ctx.right); + int type = ctx.op.getType(); + boolean boolModifier = ctx.BOOL() != null; + boolean leftIsScalar = isScalar(leftResult); + boolean rightIsScalar = isScalar(rightResult); + List<Map<String, Object>> results = new ArrayList<>(); + + // scalar and scalar + if (leftIsScalar && rightIsScalar) { + if (!boolModifier) { + // Between two scalars, + // the bool modifier must be provided and these operators result in another scalar that is either 0 (false) or 1 (true), depending on the comparison result. + return results; + } + Object leftVal = leftResult.get(0).get(SCALAR); + Object rightVal = rightResult.get(0).get(SCALAR); + Boolean match = compareOp(leftVal, type, rightVal); + // returns a result only if the comparison condition is met. + Map<String, Object> result = new HashMap<>(); + result.put(VALUE, match ? 1 : 0); + results.add(result); + return results; + } + + // scalar and vector + if (leftIsScalar) { + Object leftVal = leftResult.get(0).get(SCALAR); + for (Map<String, Object> rightItem : rightResult) { + Object rightVal = rightItem.getOrDefault(VALUE, null); + if (isValidValue(rightVal)) { + continue; + } + Boolean match = compareOp(leftVal, type, rightVal); + Map<String, Object> result = new HashMap<>(rightItem); + if (boolModifier) { + result.put(VALUE, match ? 1 : 0); + results.add(result); + } else { + result.put(VALUE, match ? rightVal : null); + results.add(result); + } + } + return results; + } + + // vector and scalar + if (rightIsScalar) { + Object rightVal = rightResult.get(0).get(SCALAR); + for (Map<String, Object> leftItem : leftResult) { + Object leftVal = leftItem.getOrDefault(VALUE, null); + if (isValidValue(leftVal)) { + continue; + } + Boolean match = compareOp(leftVal, type, rightVal); + Map<String, Object> result = new HashMap<>(leftItem); + if (boolModifier) { + result.put(VALUE, match ? 1 : 0); + results.add(result); + } else { + result.put(VALUE, match ? leftVal : null); + results.add(result); + } + } + return results; + } + + // vector and vector + Map<String, Map<String, Object>> rightMap = rightResult.stream() + .filter(item -> item.get(VALUE) != null) + .collect(Collectors.toMap(this::labelKey, item -> item, (existing, replacement) -> existing)); + + for (Map<String, Object> leftItem : leftResult) { + Object leftVal = leftItem.getOrDefault(VALUE, null); + if (isValidValue(leftVal)) { + continue; + } + Map<String, Object> rightItem = rightMap.get(labelKey(leftItem)); + if (rightItem == null) { + continue; + } + Object rightVal = rightItem.get(VALUE); + if (isValidValue(rightVal)) { + continue; + } + Boolean match = compareOp(leftVal, type, rightVal); + Map<String, Object> result = new HashMap<>(leftItem); + if (boolModifier) { + result.put(VALUE, match ? 1 : 0); + results.add(result); + } else { + result.put(VALUE, match ? leftVal : null); + results.add(result); + } + } + return results; + } + + @Override + public List<Map<String, Object>> visitAndExpr(AlertExpressionParser.AndExprContext ctx) { + List<Map<String, Object>> leftOperand = visit(ctx.left); + List<Map<String, Object>> rightOperand = visit(ctx.right); + List<Map<String, Object>> results = new ArrayList<>(); + + // build a hash set of the right-side tag collection + Set<String> rightLabelsSet = rightOperand.stream() + .filter(item -> item.get(VALUE) != null) + .map(this::labelKey) + .collect(Collectors.toSet()); + + // iterate over the left side, O(1) match + for (Map<String, Object> leftItem : leftOperand) { + Object leftVal = leftItem.get(VALUE); + if (leftVal == null) { + continue; + } + String labelKey = labelKey(leftItem); + if (rightLabelsSet.contains(labelKey)) { + results.add(new HashMap<>(leftItem)); + } + } + return results; + } + + @Override + public List<Map<String, Object>> visitOrExpr(AlertExpressionParser.OrExprContext ctx) { + List<Map<String, Object>> leftOperand = visit(ctx.left); + List<Map<String, Object>> rightOperand = visit(ctx.right); + + // build a hashMap of the left-hand label collection + Map<String, Map<String, Object>> leftLabelMap = leftOperand.stream() + .filter(item -> item.get(VALUE) != null) + .collect(Collectors.toMap(this::labelKey, HashMap::new, (k1, k2) -> k1)); + + // first add all the non-empty items on the left side + List<Map<String, Object>> results = new ArrayList<>(leftLabelMap.values()); + + // add the term that has a value on the right side and not on the left side + for (Map<String, Object> rightItem : rightOperand) { + Object rightVal = rightItem.get(VALUE); + if (rightVal == null) { + continue; + } + String key = labelKey(rightItem); + if (!leftLabelMap.containsKey(key)) { + results.add(new HashMap<>(rightItem)); + } + } + return results; + } + + @Override + public List<Map<String, Object>> visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) { + List<Map<String, Object>> leftOperand = visit(ctx.left); + List<Map<String, Object>> rightOperand = visit(ctx.right); + List<Map<String, Object>> results = new ArrayList<>(); + + // build a hash set of the right-side tag collection + Set<String> rightLabelSet = rightOperand.stream() + .filter(item -> item.get(VALUE) != null) + .map(this::labelKey) + .collect(Collectors.toSet()); + + // iterate over the left side, O(1) match + for (Map<String, Object> leftItem : leftOperand) { + Object leftVal = leftItem.get(VALUE); + if (leftVal == null) { + continue; + } + if (!rightLabelSet.contains(labelKey(leftItem))) { + results.add(new HashMap<>(leftItem)); + } + } + return results; + } + + @Override + public List<Map<String, Object>> visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx) { + double value = Double.parseDouble(ctx.number().getText()); + List<Map<String, Object>> numAsList = new ArrayList<>(); + Map<String, Object> valueMap = new HashMap<>(); + valueMap.put(SCALAR, value); + numAsList.add(valueMap); + return numAsList; + } + + @Override + public List<Map<String, Object>> visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) { + String rawPromql = tokens.getText(ctx.promql()); + return executor.execute(rawPromql); + } + + @Override + public List<Map<String, Object>> visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) { + String rawSql = tokens.getText(ctx.selectSql()); + return executor.execute(rawSql); + } + + @Override + public List<Map<String, Object>> visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) { + return callSqlOrPromql(tokens.getText(ctx.string())); + } + + @Override + public List<Map<String, Object>> visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) { + return callSqlOrPromql(tokens.getText(ctx.string())); + } + + private List<Map<String, Object>> callSqlOrPromql(String text) { + String script = text.substring(1, text.length() - 1); + return executor.execute(script); + } + + /** + * Generate tag key (excluding `__name__` and `__value__` and `__timestamp__`) + */ + private String labelKey(Map<String, Object> labelsMap) { + if (null == labelsMap || labelsMap.isEmpty()) { + return "-"; + } + String key = labelsMap.entrySet().stream() + .filter(e -> !e.getKey().equals(VALUE) && !e.getKey().equals(NAME) && !e.getKey().equals(TIMESTAMP)) + .sorted(Map.Entry.comparingByKey()) + .map(e -> e.getKey() + "=" + (e.getValue() == null ? "" : e.getValue())) + .collect(Collectors.joining(",")); + return key.isEmpty() ? "-" : key; + } + + private boolean isScalar(List<Map<String, Object>> context) { + return CollectionUtils.isNotEmpty(context) + && context.size() == 1 + && context.get(0).containsKey(SCALAR) + && null != context.get(0).get(SCALAR); + } + + private double parseStrToDouble(String text) { + try { + return Double.parseDouble(text); + } catch (NumberFormatException e) { + throw new ExpressionVisitorException("number format exception", e); + } + } + + private boolean isValidValue(Object val) { + return val == null || val instanceof List<?>; + } + + private Boolean compareOp(Object leftVal, int opType, Object rightVal) { + double left = parseStrToDouble(leftVal.toString()); + double right = parseStrToDouble(rightVal.toString()); + return switch (opType) { + case AlertExpressionParser.GT -> left > right; + case AlertExpressionParser.GE -> left >= right; + case AlertExpressionParser.LT -> left < right; + case AlertExpressionParser.LE -> left <= right; + case AlertExpressionParser.EQ -> left == right; + case AlertExpressionParser.NE -> left != right; + default -> false; + }; + } +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionLexer.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionLexer.java new file mode 100644 index 0000000..a18705b --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionLexer.java @@ -0,0 +1,142 @@ +/* + * 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. + */ + +// Generated from AlertExpression.g4 by ANTLR 4.13.2 + +package org.apache.hertzbeat.alert.expr; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.Lexer; +import org.antlr.v4.runtime.RuntimeMetaData; +import org.antlr.v4.runtime.Vocabulary; +import org.antlr.v4.runtime.VocabularyImpl; +import org.antlr.v4.runtime.atn.ATN; +import org.antlr.v4.runtime.atn.ATNDeserializer; +import org.antlr.v4.runtime.atn.LexerATNSimulator; +import org.antlr.v4.runtime.atn.PredictionContextCache; +import org.antlr.v4.runtime.dfa.DFA; + +@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue", "this-escape"}) +public class AlertExpressionLexer extends Lexer { + static { + RuntimeMetaData.checkVersion("4.13.2", RuntimeMetaData.VERSION); + } + + protected static final DFA[] _decisionToDFA; + protected static final PredictionContextCache _sharedContextCache = new PredictionContextCache(); + public static final int AND = 1, OR = 2, UNLESS = 3, NOT = 4, SELECT = 5, FROM = 6, WHERE = 7, GROUP = 8, BY = 9, HAVING = 10, ORDER = 11, LIMIT = 12, OFFSET = 13, AS = 14, ASC = 15, DESC = 16, IN = 17, IS = 18, NULL = 19, LIKE = 20, BETWEEN = 21, STAR = 22, COUNT = 23, SUM = 24, AVG = 25, MIN = 26, MAX = 27, STDDEV = 28, STDVAR = 29, VARIANCE = 30, RATE_FUNCTION = 31, INCREASE_FUNCTION = 32, HISTOGRAM_QUANTILE_FUNCTION = 33, TOPK = 34, BOTTOMK = 35, QUANTILE = 36, BY_FUNCTION = 37, WITHOUT_FUNCTION = 38, GROUP_LEFT_FUNCTION = 39, GROUP_RIGHT_FUNCTION = 40, IGNORING_FUNCTION = 41, ON_FUNCTION = 42, SQL_FUNCTION = 43, PROMQL_FUNCTION = 44, GT = 45, GE = 46, LT = 47, LE = 48, EQ = 49, NE = 50, BOOL = 51, LPAREN = 52, RPAREN = 53, LBRACE = 54, RBRACE = 55, LBRACKET = 56, RBRACKET = 57, COMMA = 58, DOT = 59, COLON = 60, SEMICOLON = 61, SCIENTIFIC_NUMBER = 62, FLOAT = 63, NUMBER = 64, DURATION = 65, STRING = 66, IDENTIFIER = 67, WS = 68, LINE_COMMENT = 69, BLOCK_COMMENT = 70; + public static String[] channelNames = {"DEFAULT_TOKEN_CHANNEL", "HIDDEN"}; + + public static String[] modeNames = {"DEFAULT_MODE"}; + + private static String[] makeRuleNames() { + return new String[]{"AND", "OR", "UNLESS", "NOT", "SELECT", "FROM", "WHERE", "GROUP", "BY", "HAVING", "ORDER", "LIMIT", "OFFSET", "AS", "ASC", "DESC", "IN", "IS", "NULL", "LIKE", "BETWEEN", "STAR", "COUNT", "SUM", "AVG", "MIN", "MAX", "STDDEV", "STDVAR", "VARIANCE", "RATE_FUNCTION", "INCREASE_FUNCTION", "HISTOGRAM_QUANTILE_FUNCTION", "TOPK", "BOTTOMK", "QUANTILE", "BY_FUNCTION", "WITHOUT_FUNCTION", "GROUP_LEFT_FUNCTION", "GROUP_RIGHT_FUNCTION", "IGNORING_FUNCTION", "ON_FUNCTION", "SQL_FUNCTION", "PROMQL_FUNCTION", "GT", "GE", "LT", "LE", "EQ", "NE", "BOOL", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET", "RBRACKET", "COMMA", "DOT", "COLON", "SEMICOLON", "SCIENTIFIC_NUMBER", "FLOAT", "NUMBER", "DURATION", "STRING", "IDENTIFIER", "WS", "LINE_COMMENT", "BLOCK_COMMENT"}; + } + + public static final String[] ruleNames = makeRuleNames(); + + private static String[] makeLiteralNames() { + return new String[]{null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "'*'", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "'>'", "'>='", "'<'", "'<='", null, "'!='", "'bool'", "'('", "')'", "'{'", "'}'", "'['", "']'", "','", "'.'", "':'", "';'"}; + } + + private static final String[] _LITERAL_NAMES = makeLiteralNames(); + + private static String[] makeSymbolicNames() { + return new String[]{null, "AND", "OR", "UNLESS", "NOT", "SELECT", "FROM", "WHERE", "GROUP", "BY", "HAVING", "ORDER", "LIMIT", "OFFSET", "AS", "ASC", "DESC", "IN", "IS", "NULL", "LIKE", "BETWEEN", "STAR", "COUNT", "SUM", "AVG", "MIN", "MAX", "STDDEV", "STDVAR", "VARIANCE", "RATE_FUNCTION", "INCREASE_FUNCTION", "HISTOGRAM_QUANTILE_FUNCTION", "TOPK", "BOTTOMK", "QUANTILE", "BY_FUNCTION", "WITHOUT_FUNCTION", "GROUP_LEFT_FUNCTION", "GROUP_RIGHT_FUNCTION", "IGNORING_FUNCTION", "ON_FUNCTION", "SQL_FUNCTION", "PROMQL_FUNCTION", "GT", "GE", "LT", "LE", "EQ", "NE", "BOOL", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET", "RBRACKET", "COMMA", "DOT", "COLON", "SEMICOLON", "SCIENTIFIC_NUMBER", "FLOAT", "NUMBER", "DURATION", "STRING", "IDENTIFIER", "WS", "LINE_COMMENT", "BLOCK_COMMENT"}; + } + + private static final String[] _SYMBOLIC_NAMES = makeSymbolicNames(); + public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES); + + /** + * @deprecated Use {@link #VOCABULARY} instead. + */ + @Deprecated + public static final String[] tokenNames; + + static { + tokenNames = new String[_SYMBOLIC_NAMES.length]; + for (int i = 0; i < tokenNames.length; i++) { + tokenNames[i] = VOCABULARY.getLiteralName(i); + if (tokenNames[i] == null) { + tokenNames[i] = VOCABULARY.getSymbolicName(i); + } + + if (tokenNames[i] == null) { + tokenNames[i] = "<INVALID>"; + } + } + } + + @Override + @Deprecated + public String[] getTokenNames() { + return tokenNames; + } + + @Override + + public Vocabulary getVocabulary() { + return VOCABULARY; + } + + + public AlertExpressionLexer(CharStream input) { + super(input); + _interp = new LexerATNSimulator(this, _ATN, _decisionToDFA, _sharedContextCache); + } + + @Override + public String getGrammarFileName() { + return "AlertExpression.g4"; + } + + @Override + public String[] getRuleNames() { + return ruleNames; + } + + @Override + public String getSerializedATN() { + return _serializedATN; + } + + @Override + public String[] getChannelNames() { + return channelNames; + } + + @Override + public String[] getModeNames() { + return modeNames; + } + + @Override + public ATN getATN() { + return _ATN; + } + + public static final String _serializedATN = "\u0004\u0000F\u022f\u0006\uffff\uffff\u0002\u0000\u0007\u0000\u0002\u0001" + "\u0007\u0001\u0002\u0002\u0007\u0002\u0002\u0003\u0007\u0003\u0002\u0004" + "\u0007\u0004\u0002\u0005\u0007\u0005\u0002\u0006\u0007\u0006\u0002\u0007" + "\u0007\u0007\u0002\b\u0007\b\u0002\t\u0007\t\u0002\n\u0007\n\u0002\u000b" + "\u0007\u000b\u0002\f\u0007\f\u0002\r\u0007\r\u0002\u000e\u0007\u000e\u0002" + "\u000f\u0007\u000f\u0002\u0010\u0007\u0010\u0002\u0011\u0007\u0011\u0002" + "\u0012\u0007\u0012\u0002\u0013\u0007\u0013\u0002\u0014\u0007\u0014\u0002" + "\u0015\u0007\u0015\u0002\u0016\u0007\u0016\u0002\u0017\u0007\u0017\u0002" + "\u0018\u0007\u0018\u0002\u0019\u0007\u0019\u0002\u001a\u0007\u001a\u0002" + "\u001b\u0007\u001b\u0002\u001c\u0007\u001c\u0002\u001d\u0007\u001d\u0002" + "\u001e\u0007\u001e\u0002\u001f\u0007\u001f\u0002 \u0007 \u0002!\u0007" + "!\u0002\"\u0007\"\u0002#\u0007#\u0002$\u0007$\u0002%\u0007%\u0002&\u0007" + "&\u0002\'\u0007\'\u0002(\u0007(\u0002)\u0007)\u0002*\u0007*\u0002+\u0007" + "+\u0002,\u0007,\u0002-\u0007-\u0002.\u0007.\u0002/\u0007/\u00020\u0007" + "0\u00021\u00071\u00022\u00072\u00023\u00073\u00024\u00074\u00025\u0007" + "5\u00026\u00076\u00027\u00077\u00028\u00078\u00029\u00079\u0002:\u0007" + ":\u0002;\u0007;\u0002<\u0007<\u0002=\u0007=\u0002>\u0007>\u0002?\u0007" + "?\u0002@\u0007@\u0002A\u0007A\u0002B\u0007B\u0002C\u0007C\u0002D\u0007" + "D\u0002E\u0007E\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001" + "\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u0001\u0002\u0001\u0002\u0001" + "\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0003\u0001\u0003\u0001" + "\u0003\u0001\u0003\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004\u0001" + "\u0004\u0001\u0004\u0001\u0004\u0001\u0005\u0001\u0005\u0001\u0005\u0001" + "\u0005\u0001\u0005\u0001\u0006\u0001\u0006\u0001\u0006\u0001\u0006\u0001" + "\u0006\u0001\u0006\u0001\u0007\u0001\u0007\u0001\u0007\u0001\u0007\u0001" + "\u0007\u0001\u0007\u0001\b\u0001\b\u0001\b\u0001\t\u0001\t\u0001\t\u0001" + "\t\u0001\t\u0001\t\u0001\t\u0001\n\u0001\n\u0001\n\u0001\n\u0001\n\u0001" + "\n\u0001\u000b\u0001\u000b\u0001\u000b\u0001\u000b\u0001\u000b\u0001\u000b" + "\u0001\f\u0001\f\u0001\f\u0001\f\u0001\f\u0001\f\u0001\f\u0001\r\u0001" + "\r\u0001\r\u0001\u000e\u0001\u000e\u0001\u000e\u0001\u000e\u0001\u000f" + "\u0001\u000f\u0001\u000f\u0001\u000f\u0001\u000f\u0001\u0010\u0001\u0010" + "\u0001\u0010\u0001\u0011\u0001\u0011\u0001\u0011\u0001\u0012\u0001\u0012" + "\u0001\u0012\u0001\u0012\u0001\u0012\u0001\u0013\u0001\u0013\u0001\u0013" + "\u0001\u0013\u0001\u0013\u0001\u0014\u0001\u0014\u0001\u0014\u0001\u0014" + "\u0001\u0014\u0001\u0014\u0001\u0014\u0001\u0014\u0001\u0015\u0001\u0015" + "\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0016" + "\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0018\u0001\u0018" + "\u0001\u0018\u0001\u0018\u0001\u0019\u0001\u0019\u0001\u0019\u0001\u0019" + "\u0001\u001a\u0001\u001a\u0001\u001a\u0001\u001a\u0001\u001b\u0001\u001b" + "\u0001\u001b\u0001\u001b\u0001\u001b\u0001\u001b\u0001\u001b\u0001\u001c" + "\u0001\u001c\u0001\u001c\u0001\u001c\u0001\u001c\u0001\u001c\u0001\u001c" + "\u0001\u001d\u0001\u001d\u0001\u001d\u0001\u001d\u0001\u001d\u0001\u001d" + "\u0001\u001d\u0001\u001d\u0001\u001d\u0001\u001e\u0001\u001e\u0001\u001e" + "\u0001\u001e\u0001\u001e\u0001\u001f\u0001\u001f\u0001\u001f\u0001\u001f" + "\u0001\u001f\u0001\u001f\u0001\u001f\u0001\u001f\u0001\u001f\u0001 \u0001" + " \u0001 \u0001 \u0001 \u0001 \u0001 \u0001 \u0001 \u0001 \u0001 \u0001" + " \u0001 \u0001 \u0001 \u0001 \u0001 \u0001 \u0001 \u0001!\u0001!\u0001" + "!\u0001!\u0001!\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001" + "\"\u0001\"\u0001#\u0001#\u0001#\u0001#\u0001#\u0001#\u0001#\u0001#\u0001" + "#\u0001$\u0001$\u0001$\u0001%\u0001%\u0001%\u0001%\u0001%\u0001%\u0001" + "%\u0001%\u0001&\u0001&\u0001&\u0001&\u0001&\u0001&\u0001&\u0001&\u0001" + "&\u0001&\u0001&\u0001\'\u0001\'\u0001\'\u0001\'\u0001\'\u0001\'\u0001" + "\'\u0001\'\u0001\'\u0001\'\u0001\'\u0001\'\u0001(\u0001(\u0001(\u0001" + "(\u0001(\u0001(\u0001(\u0001(\u0001(\u0001)\u0001)\u0001)\u0001*\u0001" + "*\u0001*\u0001*\u0001+\u0001+\u0001+\u0001+\u0001+\u0001+\u0001+\u0001" + ",\u0001,\u0001-\u0001-\u0001-\u0001.\u0001.\u0001/\u0001/\u0001/\u0001" + "0\u00010\u00010\u00030\u01a5\b0\u00011\u00011\u00011\u00012\u00012\u0001" + "2\u00012\u00012\u00013\u00013\u00014\u00014\u00015\u00015\u00016\u0001" + "6\u00017\u00017\u00018\u00018\u00019\u00019\u0001:\u0001:\u0001;\u0001" + ";\u0001<\u0001<\u0001=\u0004=\u01c4\b=\u000b=\f=\u01c5\u0001=\u0001=\u0004" + "=\u01ca\b=\u000b=\f=\u01cb\u0003=\u01ce\b=\u0001=\u0001=\u0003=\u01d2" + "\b=\u0001=\u0004=\u01d5\b=\u000b=\f=\u01d6\u0001>\u0004>\u01da\b>\u000b" + ">\f>\u01db\u0001>\u0001>\u0004>\u01e0\b>\u000b>\f>\u01e1\u0001?\u0004" + "?\u01e5\b?\u000b?\f?\u01e6\u0001@\u0004@\u01ea\b@\u000b@\f@\u01eb\u0001" + "@\u0004@\u01ef\b@\u000b@\f@\u01f0\u0001A\u0001A\u0001A\u0001A\u0005A\u01f7" + "\bA\nA\fA\u01fa\tA\u0001A\u0001A\u0001A\u0001A\u0001A\u0005A\u0201\bA" + "\nA\fA\u0204\tA\u0001A\u0003A\u0207\bA\u0001B\u0001B\u0005B\u020b\bB\n" + "B\fB\u020e\tB\u0001C\u0004C\u0211\bC\u000bC\fC\u0212\u0001C\u0001C\u0001" + "D\u0001D\u0001D\u0001D\u0005D\u021b\bD\nD\fD\u021e\tD\u0001D\u0001D\u0001" + "E\u0001E\u0001E\u0001E\u0005E\u0226\bE\nE\fE\u0229\tE\u0001E\u0001E\u0001" + "E\u0001E\u0001E\u0001\u0227\u0000F\u0001\u0001\u0003\u0002\u0005\u0003" + "\u0007\u0004\t\u0005\u000b\u0006\r\u0007\u000f\b\u0011\t\u0013\n\u0015" + "\u000b\u0017\f\u0019\r\u001b\u000e\u001d\u000f\u001f\u0010!\u0011#\u0012" + "%\u0013\'\u0014)\u0015+\u0016-\u0017/\u00181\u00193\u001a5\u001b7\u001c" + "9\u001d;\u001e=\u001f? A!C\"E#G$I%K&M\'O(Q)S*U+W,Y-[.]/_0a1c2e3g4i5k6" + "m7o8q9s:u;w<y={>}?\u007f@\u0081A\u0083B\u0085C\u0087D\u0089E\u008bF\u0001" + "\u0000!\u0002\u0000AAaa\u0002\u0000NNnn\u0002\u0000DDdd\u0002\u0000OO" + "oo\u0002\u0000RRrr\u0002\u0000UUuu\u0002\u0000LLll\u0002\u0000EEee\u0002" + "\u0000SSss\u0002\u0000TTtt\u0002\u0000CCcc\u0002\u0000FFff\u0002\u0000" + "MMmm\u0002\u0000WWww\u0002\u0000HHhh\u0002\u0000GGgg\u0002\u0000PPpp\u0002" + "\u0000BBbb\u0002\u0000YYyy\u0002\u0000VVvv\u0002\u0000IIii\u0002\u0000" + "KKkk\u0002\u0000XXxx\u0002\u0000QQqq\u0001\u000009\u0002\u0000++--\u0002" + "\u0000AZaz\u0004\u0000\n\n\r\r\"\"\\\\\u0004\u0000\n\n\r\r\'\'\\\\\u0003" + "\u0000AZ__az\n\u0000\"\"-.09==A[]]__a{}~\u8000\uff5e\u8000\uff5e\u0003" + "\u0000\t\n\r\r \u0002\u0000\n\n\r\r\u0242\u0000\u0001\u0001\u0000\u0000" + "\u0000\u0000\u0003\u0001\u0000\u0000\u0000\u0000\u0005\u0001\u0000\u0000" + "\u0000\u0000\u0007\u0001\u0000\u0000\u0000\u0000\t\u0001\u0000\u0000\u0000" + "\u0000\u000b\u0001\u0000\u0000\u0000\u0000\r\u0001\u0000\u0000\u0000\u0000" + "\u000f\u0001\u0000\u0000\u0000\u0000\u0011\u0001\u0000\u0000\u0000\u0000" + "\u0013\u0001\u0000\u0000\u0000\u0000\u0015\u0001\u0000\u0000\u0000\u0000" + "\u0017\u0001\u0000\u0000\u0000\u0000\u0019\u0001\u0000\u0000\u0000\u0000" + "\u001b\u0001\u0000\u0000\u0000\u0000\u001d\u0001\u0000\u0000\u0000\u0000" + "\u001f\u0001\u0000\u0000\u0000\u0000!\u0001\u0000\u0000\u0000\u0000#\u0001" + "\u0000\u0000\u0000\u0000%\u0001\u0000\u0000\u0000\u0000\'\u0001\u0000" + "\u0000\u0000\u0000)\u0001\u0000\u0000\u0000\u0000+\u0001\u0000\u0000\u0000" + "\u0000-\u0001\u0000\u0000\u0000\u0000/\u0001\u0000\u0000\u0000\u00001" + "\u0001\u0000\u0000\u0000\u00003\u0001\u0000\u0000\u0000\u00005\u0001\u0000" + "\u0000\u0000\u00007\u0001\u0000\u0000\u0000\u00009\u0001\u0000\u0000\u0000" + "\u0000;\u0001\u0000\u0000\u0000\u0000=\u0001\u0000\u0000\u0000\u0000?" + "\u0001\u0000\u0000\u0000\u0000A\u0001\u0000\u0000\u0000\u0000C\u0001\u0000" + "\u0000\u0000\u0000E\u0001\u0000\u0000\u0000\u0000G\u0001\u0000\u0000\u0000" + "\u0000I\u0001\u0000\u0000\u0000\u0000K\u0001\u0000\u0000\u0000\u0000M" + "\u0001\u0000\u0000\u0000\u0000O\u0001\u0000\u0000\u0000\u0000Q\u0001\u0000" + "\u0000\u0000\u0000S\u0001\u0000\u0000\u0000\u0000U\u0001\u0000\u0000\u0000" + "\u0000W\u0001\u0000\u0000\u0000\u0000Y\u0001\u0000\u0000\u0000\u0000[" + "\u0001\u0000\u0000\u0000\u0000]\u0001\u0000\u0000\u0000\u0000_\u0001\u0000" + "\u0000\u0000\u0000a\u0001\u0000\u0000\u0000\u0000c\u0001\u0000\u0000\u0000" + "\u0000e\u0001\u0000\u0000\u0000\u0000g\u0001\u0000\u0000\u0000\u0000i" + "\u0001\u0000\u0000\u0000\u0000k\u0001\u0000\u0000\u0000\u0000m\u0001\u0000" + "\u0000\u0000\u0000o\u0001\u0000\u0000\u0000\u0000q\u0001\u0000\u0000\u0000" + "\u0000s\u0001\u0000\u0000\u0000\u0000u\u0001\u0000\u0000\u0000\u0000w" + "\u0001\u0000\u0000\u0000\u0000y\u0001\u0000\u0000\u0000\u0000{\u0001\u0000" + "\u0000\u0000\u0000}\u0001\u0000\u0000\u0000\u0000\u007f\u0001\u0000\u0000" + "\u0000\u0000\u0081\u0001\u0000\u0000\u0000\u0000\u0083\u0001\u0000\u0000" + "\u0000\u0000\u0085\u0001\u0000\u0000\u0000\u0000\u0087\u0001\u0000\u0000" + "\u0000\u0000\u0089\u0001\u0000\u0000\u0000\u0000\u008b\u0001\u0000\u0000" + "\u0000\u0001\u008d\u0001\u0000\u0000\u0000\u0003\u0091\u0001\u0000\u0000" + "\u0000\u0005\u0094\u0001\u0000\u0000\u0000\u0007\u009b\u0001\u0000\u0000" + "\u0000\t\u009f\u0001\u0000\u0000\u0000\u000b\u00a6\u0001\u0000\u0000\u0000" + "\r\u00ab\u0001\u0000\u0000\u0000\u000f\u00b1\u0001\u0000\u0000\u0000\u0011" + "\u00b7\u0001\u0000\u0000\u0000\u0013\u00ba\u0001\u0000\u0000\u0000\u0015" + "\u00c1\u0001\u0000\u0000\u0000\u0017\u00c7\u0001\u0000\u0000\u0000\u0019" + "\u00cd\u0001\u0000\u0000\u0000\u001b\u00d4\u0001\u0000\u0000\u0000\u001d" + "\u00d7\u0001\u0000\u0000\u0000\u001f\u00db\u0001\u0000\u0000\u0000!\u00e0" + "\u0001\u0000\u0000\u0000#\u00e3\u0001\u0000\u0000\u0000%\u00e6\u0001\u0000" + "\u0000\u0000\'\u00eb\u0001\u0000\u0000\u0000)\u00f0\u0001\u0000\u0000" + "\u0000+\u00f8\u0001\u0000\u0000\u0000-\u00fa\u0001\u0000\u0000\u0000/" + "\u0100\u0001\u0000\u0000\u00001\u0104\u0001\u0000\u0000\u00003\u0108\u0001" + "\u0000\u0000\u00005\u010c\u0001\u0000\u0000\u00007\u0110\u0001\u0000\u0000" + "\u00009\u0117\u0001\u0000\u0000\u0000;\u011e\u0001\u0000\u0000\u0000=" + "\u0127\u0001\u0000\u0000\u0000?\u012c\u0001\u0000\u0000\u0000A\u0135\u0001" + "\u0000\u0000\u0000C\u0148\u0001\u0000\u0000\u0000E\u014d\u0001\u0000\u0000" + "\u0000G\u0155\u0001\u0000\u0000\u0000I\u015e\u0001\u0000\u0000\u0000K" + "\u0161\u0001\u0000\u0000\u0000M\u0169\u0001\u0000\u0000\u0000O\u0174\u0001" + "\u0000\u0000\u0000Q\u0180\u0001\u0000\u0000\u0000S\u0189\u0001\u0000\u0000" + "\u0000U\u018c\u0001\u0000\u0000\u0000W\u0190\u0001\u0000\u0000\u0000Y" + "\u0197\u0001\u0000\u0000\u0000[\u0199\u0001\u0000\u0000\u0000]\u019c\u0001" + "\u0000\u0000\u0000_\u019e\u0001\u0000\u0000\u0000a\u01a4\u0001\u0000\u0000" + "\u0000c\u01a6\u0001\u0000\u0000\u0000e\u01a9\u0001\u0000\u0000\u0000g" + "\u01ae\u0001\u0000\u0000\u0000i\u01b0\u0001\u0000\u0000\u0000k\u01b2\u0001" + "\u0000\u0000\u0000m\u01b4\u0001\u0000\u0000\u0000o\u01b6\u0001\u0000\u0000" + "\u0000q\u01b8\u0001\u0000\u0000\u0000s\u01ba\u0001\u0000\u0000\u0000u" + "\u01bc\u0001\u0000\u0000\u0000w\u01be\u0001\u0000\u0000\u0000y\u01c0\u0001" + "\u0000\u0000\u0000{\u01c3\u0001\u0000\u0000\u0000}\u01d9\u0001\u0000\u0000" + "\u0000\u007f\u01e4\u0001\u0000\u0000\u0000\u0081\u01e9\u0001\u0000\u0000" + "\u0000\u0083\u0206\u0001\u0000\u0000\u0000\u0085\u0208\u0001\u0000\u0000" + "\u0000\u0087\u0210\u0001\u0000\u0000\u0000\u0089\u0216\u0001\u0000\u0000" + "\u0000\u008b\u0221\u0001\u0000\u0000\u0000\u008d\u008e\u0007\u0000\u0000" + "\u0000\u008e\u008f\u0007\u0001\u0000\u0000\u008f\u0090\u0007\u0002\u0000" + "\u0000\u0090\u0002\u0001\u0000\u0000\u0000\u0091\u0092\u0007\u0003\u0000" + "\u0000\u0092\u0093\u0007\u0004\u0000\u0000\u0093\u0004\u0001\u0000\u0000" + "\u0000\u0094\u0095\u0007\u0005\u0000\u0000\u0095\u0096\u0007\u0001\u0000" + "\u0000\u0096\u0097\u0007\u0006\u0000\u0000\u0097\u0098\u0007\u0007\u0000" + "\u0000\u0098\u0099\u0007\b\u0000\u0000\u0099\u009a\u0007\b\u0000\u0000" + "\u009a\u0006\u0001\u0000\u0000\u0000\u009b\u009c\u0007\u0001\u0000\u0000" + "\u009c\u009d\u0007\u0003\u0000\u0000\u009d\u009e\u0007\t\u0000\u0000\u009e" + "\b\u0001\u0000\u0000\u0000\u009f\u00a0\u0007\b\u0000\u0000\u00a0\u00a1" + "\u0007\u0007\u0000\u0000\u00a1\u00a2\u0007\u0006\u0000\u0000\u00a2\u00a3" + "\u0007\u0007\u0000\u0000\u00a3\u00a4\u0007\n\u0000\u0000\u00a4\u00a5\u0007" + "\t\u0000\u0000\u00a5\n\u0001\u0000\u0000\u0000\u00a6\u00a7\u0007\u000b" + "\u0000\u0000\u00a7\u00a8\u0007\u0004\u0000\u0000\u00a8\u00a9\u0007\u0003" + "\u0000\u0000\u00a9\u00aa\u0007\f\u0000\u0000\u00aa\f\u0001\u0000\u0000" + "\u0000\u00ab\u00ac\u0007\r\u0000\u0000\u00ac\u00ad\u0007\u000e\u0000\u0000" + "\u00ad\u00ae\u0007\u0007\u0000\u0000\u00ae\u00af\u0007\u0004\u0000\u0000" + "\u00af\u00b0\u0007\u0007\u0000\u0000\u00b0\u000e\u0001\u0000\u0000\u0000" + "\u00b1\u00b2\u0007\u000f\u0000\u0000\u00b2\u00b3\u0007\u0004\u0000\u0000" + "\u00b3\u00b4\u0007\u0003\u0000\u0000\u00b4\u00b5\u0007\u0005\u0000\u0000" + "\u00b5\u00b6\u0007\u0010\u0000\u0000\u00b6\u0010\u0001\u0000\u0000\u0000" + "\u00b7\u00b8\u0007\u0011\u0000\u0000\u00b8\u00b9\u0007\u0012\u0000\u0000" + "\u00b9\u0012\u0001\u0000\u0000\u0000\u00ba\u00bb\u0007\u000e\u0000\u0000" + "\u00bb\u00bc\u0007\u0000\u0000\u0000\u00bc\u00bd\u0007\u0013\u0000\u0000" + "\u00bd\u00be\u0007\u0014\u0000\u0000\u00be\u00bf\u0007\u0001\u0000\u0000" + "\u00bf\u00c0\u0007\u000f\u0000\u0000\u00c0\u0014\u0001\u0000\u0000\u0000" + "\u00c1\u00c2\u0007\u0003\u0000\u0000\u00c2\u00c3\u0007\u0004\u0000\u0000" + "\u00c3\u00c4\u0007\u0002\u0000\u0000\u00c4\u00c5\u0007\u0007\u0000\u0000" + "\u00c5\u00c6\u0007\u0004\u0000\u0000\u00c6\u0016\u0001\u0000\u0000\u0000" + "\u00c7\u00c8\u0007\u0006\u0000\u0000\u00c8\u00c9\u0007\u0014\u0000\u0000" + "\u00c9\u00ca\u0007\f\u0000\u0000\u00ca\u00cb\u0007\u0014\u0000\u0000\u00cb" + "\u00cc\u0007\t\u0000\u0000\u00cc\u0018\u0001\u0000\u0000\u0000\u00cd\u00ce" + "\u0007\u0003\u0000\u0000\u00ce\u00cf\u0007\u000b\u0000\u0000\u00cf\u00d0" + "\u0007\u000b\u0000\u0000\u00d0\u00d1\u0007\b\u0000\u0000\u00d1\u00d2\u0007" + "\u0007\u0000\u0000\u00d2\u00d3\u0007\t\u0000\u0000\u00d3\u001a\u0001\u0000" + "\u0000\u0000\u00d4\u00d5\u0007\u0000\u0000\u0000\u00d5\u00d6\u0007\b\u0000" + "\u0000\u00d6\u001c\u0001\u0000\u0000\u0000\u00d7\u00d8\u0007\u0000\u0000" + "\u0000\u00d8\u00d9\u0007\b\u0000\u0000\u00d9\u00da\u0007\n\u0000\u0000" + "\u00da\u001e\u0001\u0000\u0000\u0000\u00db\u00dc\u0007\u0002\u0000\u0000" + "\u00dc\u00dd\u0007\u0007\u0000\u0000\u00dd\u00de\u0007\b\u0000\u0000\u00de" + "\u00df\u0007\n\u0000\u0000\u00df \u0001\u0000\u0000\u0000\u00e0\u00e1" + "\u0007\u0014\u0000\u0000\u00e1\u00e2\u0007\u0001\u0000\u0000\u00e2\"\u0001" + "\u0000\u0000\u0000\u00e3\u00e4\u0007\u0014\u0000\u0000\u00e4\u00e5\u0007" + "\b\u0000\u0000\u00e5$\u0001\u0000\u0000\u0000\u00e6\u00e7\u0007\u0001" + "\u0000\u0000\u00e7\u00e8\u0007\u0005\u0000\u0000\u00e8\u00e9\u0007\u0006" + "\u0000\u0000\u00e9\u00ea\u0007\u0006\u0000\u0000\u00ea&\u0001\u0000\u0000" + "\u0000\u00eb\u00ec\u0007\u0006\u0000\u0000\u00ec\u00ed\u0007\u0014\u0000" + "\u0000\u00ed\u00ee\u0007\u0015\u0000\u0000\u00ee\u00ef\u0007\u0007\u0000" + "\u0000\u00ef(\u0001\u0000\u0000\u0000\u00f0\u00f1\u0007\u0011\u0000\u0000" + "\u00f1\u00f2\u0007\u0007\u0000\u0000\u00f2\u00f3\u0007\t\u0000\u0000\u00f3" + "\u00f4\u0007\r\u0000\u0000\u00f4\u00f5\u0007\u0007\u0000\u0000\u00f5\u00f6" + "\u0007\u0007\u0000\u0000\u00f6\u00f7\u0007\u0001\u0000\u0000\u00f7*\u0001" + "\u0000\u0000\u0000\u00f8\u00f9\u0005*\u0000\u0000\u00f9,\u0001\u0000\u0000" + "\u0000\u00fa\u00fb\u0007\n\u0000\u0000\u00fb\u00fc\u0007\u0003\u0000\u0000" + "\u00fc\u00fd\u0007\u0005\u0000\u0000\u00fd\u00fe\u0007\u0001\u0000\u0000" + "\u00fe\u00ff\u0007\t\u0000\u0000\u00ff.\u0001\u0000\u0000\u0000\u0100" + "\u0101\u0007\b\u0000\u0000\u0101\u0102\u0007\u0005\u0000\u0000\u0102\u0103" + "\u0007\f\u0000\u0000\u01030\u0001\u0000\u0000\u0000\u0104\u0105\u0007" + "\u0000\u0000\u0000\u0105\u0106\u0007\u0013\u0000\u0000\u0106\u0107\u0007" + "\u000f\u0000\u0000\u01072\u0001\u0000\u0000\u0000\u0108\u0109\u0007\f" + "\u0000\u0000\u0109\u010a\u0007\u0014\u0000\u0000\u010a\u010b\u0007\u0001" + "\u0000\u0000\u010b4\u0001\u0000\u0000\u0000\u010c\u010d\u0007\f\u0000" + "\u0000\u010d\u010e\u0007\u0000\u0000\u0000\u010e\u010f\u0007\u0016\u0000" + "\u0000\u010f6\u0001\u0000\u0000\u0000\u0110\u0111\u0007\b\u0000\u0000" + "\u0111\u0112\u0007\t\u0000\u0000\u0112\u0113\u0007\u0002\u0000\u0000\u0113" + "\u0114\u0007\u0002\u0000\u0000\u0114\u0115\u0007\u0007\u0000\u0000\u0115" + "\u0116\u0007\u0013\u0000\u0000\u01168\u0001\u0000\u0000\u0000\u0117\u0118" + "\u0007\b\u0000\u0000\u0118\u0119\u0007\t\u0000\u0000\u0119\u011a\u0007" + "\u0002\u0000\u0000\u011a\u011b\u0007\u0013\u0000\u0000\u011b\u011c\u0007" + "\u0000\u0000\u0000\u011c\u011d\u0007\u0004\u0000\u0000\u011d:\u0001\u0000" + "\u0000\u0000\u011e\u011f\u0007\u0013\u0000\u0000\u011f\u0120\u0007\u0000" + "\u0000\u0000\u0120\u0121\u0007\u0004\u0000\u0000\u0121\u0122\u0007\u0014" + "\u0000\u0000\u0122\u0123\u0007\u0000\u0000\u0000\u0123\u0124\u0007\u0001" + "\u0000\u0000\u0124\u0125\u0007\n\u0000\u0000\u0125\u0126\u0007\u0007\u0000" + "\u0000\u0126<\u0001\u0000\u0000\u0000\u0127\u0128\u0007\u0004\u0000\u0000" + "\u0128\u0129\u0007\u0000\u0000\u0000\u0129\u012a\u0007\t\u0000\u0000\u012a" + "\u012b\u0007\u0007\u0000\u0000\u012b>\u0001\u0000\u0000\u0000\u012c\u012d" + "\u0007\u0014\u0000\u0000\u012d\u012e\u0007\u0001\u0000\u0000\u012e\u012f" + "\u0007\n\u0000\u0000\u012f\u0130\u0007\u0004\u0000\u0000\u0130\u0131\u0007" + "\u0007\u0000\u0000\u0131\u0132\u0007\u0000\u0000\u0000\u0132\u0133\u0007" + "\b\u0000\u0000\u0133\u0134\u0007\u0007\u0000\u0000\u0134@\u0001\u0000" + "\u0000\u0000\u0135\u0136\u0007\u000e\u0000\u0000\u0136\u0137\u0007\u0014" + "\u0000\u0000\u0137\u0138\u0007\b\u0000\u0000\u0138\u0139\u0007\t\u0000" + "\u0000\u0139\u013a\u0007\u0003\u0000\u0000\u013a\u013b\u0007\u000f\u0000" + "\u0000\u013b\u013c\u0007\u0004\u0000\u0000\u013c\u013d\u0007\u0000\u0000" + "\u0000\u013d\u013e\u0007\f\u0000\u0000\u013e\u013f\u0005_\u0000\u0000" + "\u013f\u0140\u0007\u0017\u0000\u0000\u0140\u0141\u0007\u0005\u0000\u0000" + "\u0141\u0142\u0007\u0000\u0000\u0000\u0142\u0143\u0007\u0001\u0000\u0000" + "\u0143\u0144\u0007\t\u0000\u0000\u0144\u0145\u0007\u0014\u0000\u0000\u0145" + "\u0146\u0007\u0006\u0000\u0000\u0146\u0147\u0007\u0007\u0000\u0000\u0147" + "B\u0001\u0000\u0000\u0000\u0148\u0149\u0007\t\u0000\u0000\u0149\u014a" + "\u0007\u0003\u0000\u0000\u014a\u014b\u0007\u0010\u0000\u0000\u014b\u014c" + "\u0007\u0015\u0000\u0000\u014cD\u0001\u0000\u0000\u0000\u014d\u014e\u0007" + "\u0011\u0000\u0000\u014e\u014f\u0007\u0003\u0000\u0000\u014f\u0150\u0007" + "\t\u0000\u0000\u0150\u0151\u0007\t\u0000\u0000\u0151\u0152\u0007\u0003" + "\u0000\u0000\u0152\u0153\u0007\f\u0000\u0000\u0153\u0154\u0007\u0015\u0000" + "\u0000\u0154F\u0001\u0000\u0000\u0000\u0155\u0156\u0007\u0017\u0000\u0000" + "\u0156\u0157\u0007\u0005\u0000\u0000\u0157\u0158\u0007\u0000\u0000\u0000" + "\u0158\u0159\u0007\u0001\u0000\u0000\u0159\u015a\u0007\t\u0000\u0000\u015a" + "\u015b\u0007\u0014\u0000\u0000\u015b\u015c\u0007\u0006\u0000\u0000\u015c" + "\u015d\u0007\u0007\u0000\u0000\u015dH\u0001\u0000\u0000\u0000\u015e\u015f" + "\u0007\u0011\u0000\u0000\u015f\u0160\u0007\u0012\u0000\u0000\u0160J\u0001" + "\u0000\u0000\u0000\u0161\u0162\u0007\r\u0000\u0000\u0162\u0163\u0007\u0014" + "\u0000\u0000\u0163\u0164\u0007\t\u0000\u0000\u0164\u0165\u0007\u000e\u0000" + "\u0000\u0165\u0166\u0007\u0003\u0000\u0000\u0166\u0167\u0007\u0005\u0000" + "\u0000\u0167\u0168\u0007\t\u0000\u0000\u0168L\u0001\u0000\u0000\u0000" + "\u0169\u016a\u0007\u000f\u0000\u0000\u016a\u016b\u0007\u0004\u0000\u0000" + "\u016b\u016c\u0007\u0003\u0000\u0000\u016c\u016d\u0007\u0005\u0000\u0000" + "\u016d\u016e\u0007\u0010\u0000\u0000\u016e\u016f\u0005_\u0000\u0000\u016f" + "\u0170\u0007\u0006\u0000\u0000\u0170\u0171\u0007\u0007\u0000\u0000\u0171" + "\u0172\u0007\u000b\u0000\u0000\u0172\u0173\u0007\t\u0000\u0000\u0173N" + "\u0001\u0000\u0000\u0000\u0174\u0175\u0007\u000f\u0000\u0000\u0175\u0176" + "\u0007\u0004\u0000\u0000\u0176\u0177\u0007\u0003\u0000\u0000\u0177\u0178" + "\u0007\u0005\u0000\u0000\u0178\u0179\u0007\u0010\u0000\u0000\u0179\u017a" + "\u0005_\u0000\u0000\u017a\u017b\u0007\u0004\u0000\u0000\u017b\u017c\u0007" + "\u0014\u0000\u0000\u017c\u017d\u0007\u000f\u0000\u0000\u017d\u017e\u0007" + "\u000e\u0000\u0000\u017e\u017f\u0007\t\u0000\u0000\u017fP\u0001\u0000" + "\u0000\u0000\u0180\u0181\u0007\u0014\u0000\u0000\u0181\u0182\u0007\u000f" + "\u0000\u0000\u0182\u0183\u0007\u0001\u0000\u0000\u0183\u0184\u0007\u0003" + "\u0000\u0000\u0184\u0185\u0007\u0004\u0000\u0000\u0185\u0186\u0007\u0014" + "\u0000\u0000\u0186\u0187\u0007\u0001\u0000\u0000\u0187\u0188\u0007\u000f" + "\u0000\u0000\u0188R\u0001\u0000\u0000\u0000\u0189\u018a\u0007\u0003\u0000" + "\u0000\u018a\u018b\u0007\u0001\u0000\u0000\u018bT\u0001\u0000\u0000\u0000" + "\u018c\u018d\u0007\b\u0000\u0000\u018d\u018e\u0007\u0017\u0000\u0000\u018e" + "\u018f\u0007\u0006\u0000\u0000\u018fV\u0001\u0000\u0000\u0000\u0190\u0191" + "\u0007\u0010\u0000\u0000\u0191\u0192\u0007\u0004\u0000\u0000\u0192\u0193" + "\u0007\u0003\u0000\u0000\u0193\u0194\u0007\f\u0000\u0000\u0194\u0195\u0007" + "\u0017\u0000\u0000\u0195\u0196\u0007\u0006\u0000\u0000\u0196X\u0001\u0000" + "\u0000\u0000\u0197\u0198\u0005>\u0000\u0000\u0198Z\u0001\u0000\u0000\u0000" + "\u0199\u019a\u0005>\u0000\u0000\u019a\u019b\u0005=\u0000\u0000\u019b\\" + "\u0001\u0000\u0000\u0000\u019c\u019d\u0005<\u0000\u0000\u019d^\u0001\u0000" + "\u0000\u0000\u019e\u019f\u0005<\u0000\u0000\u019f\u01a0\u0005=\u0000\u0000" + "\u01a0`\u0001\u0000\u0000\u0000\u01a1\u01a2\u0005=\u0000\u0000\u01a2\u01a5" + "\u0005=\u0000\u0000\u01a3\u01a5\u0005=\u0000\u0000\u01a4\u01a1\u0001\u0000" + "\u0000\u0000\u01a4\u01a3\u0001\u0000\u0000\u0000\u01a5b\u0001\u0000\u0000" + "\u0000\u01a6\u01a7\u0005!\u0000\u0000\u01a7\u01a8\u0005=\u0000\u0000\u01a8" + "d\u0001\u0000\u0000\u0000\u01a9\u01aa\u0005b\u0000\u0000\u01aa\u01ab\u0005" + "o\u0000\u0000\u01ab\u01ac\u0005o\u0000\u0000\u01ac\u01ad\u0005l\u0000" + "\u0000\u01adf\u0001\u0000\u0000\u0000\u01ae\u01af\u0005(\u0000\u0000\u01af" + "h\u0001\u0000\u0000\u0000\u01b0\u01b1\u0005)\u0000\u0000\u01b1j\u0001" + "\u0000\u0000\u0000\u01b2\u01b3\u0005{\u0000\u0000\u01b3l\u0001\u0000\u0000" + "\u0000\u01b4\u01b5\u0005}\u0000\u0000\u01b5n\u0001\u0000\u0000\u0000\u01b6" + "\u01b7\u0005[\u0000\u0000\u01b7p\u0001\u0000\u0000\u0000\u01b8\u01b9\u0005" + "]\u0000\u0000\u01b9r\u0001\u0000\u0000\u0000\u01ba\u01bb\u0005,\u0000" + "\u0000\u01bbt\u0001\u0000\u0000\u0000\u01bc\u01bd\u0005.\u0000\u0000\u01bd" + "v\u0001\u0000\u0000\u0000\u01be\u01bf\u0005:\u0000\u0000\u01bfx\u0001" + "\u0000\u0000\u0000\u01c0\u01c1\u0005;\u0000\u0000\u01c1z\u0001\u0000\u0000" + "\u0000\u01c2\u01c4\u0007\u0018\u0000\u0000\u01c3\u01c2\u0001\u0000\u0000" + "\u0000\u01c4\u01c5\u0001\u0000\u0000\u0000\u01c5\u01c3\u0001\u0000\u0000" + "\u0000\u01c5\u01c6\u0001\u0000\u0000\u0000\u01c6\u01cd\u0001\u0000\u0000" + "\u0000\u01c7\u01c9\u0005.\u0000\u0000\u01c8\u01ca\u0007\u0018\u0000\u0000" + "\u01c9\u01c8\u0001\u0000\u0000\u0000\u01ca\u01cb\u0001\u0000\u0000\u0000" + "\u01cb\u01c9\u0001\u0000\u0000\u0000\u01cb\u01cc\u0001\u0000\u0000\u0000" + "\u01cc\u01ce\u0001\u0000\u0000\u0000\u01cd\u01c7\u0001\u0000\u0000\u0000" + "\u01cd\u01ce\u0001\u0000\u0000\u0000\u01ce\u01cf\u0001\u0000\u0000\u0000" + "\u01cf\u01d1\u0007\u0007\u0000\u0000\u01d0\u01d2\u0007\u0019\u0000\u0000" + "\u01d1\u01d0\u0001\u0000\u0000\u0000\u01d1\u01d2\u0001\u0000\u0000\u0000" + "\u01d2\u01d4\u0001\u0000\u0000\u0000\u01d3\u01d5\u0007\u0018\u0000\u0000" + "\u01d4\u01d3\u0001\u0000\u0000\u0000\u01d5\u01d6\u0001\u0000\u0000\u0000" + "\u01d6\u01d4\u0001\u0000\u0000\u0000\u01d6\u01d7\u0001\u0000\u0000\u0000" + "\u01d7|\u0001\u0000\u0000\u0000\u01d8\u01da\u0007\u0018\u0000\u0000\u01d9" + "\u01d8\u0001\u0000\u0000\u0000\u01da\u01db\u0001\u0000\u0000\u0000\u01db" + "\u01d9\u0001\u0000\u0000\u0000\u01db\u01dc\u0001\u0000\u0000\u0000\u01dc" + "\u01dd\u0001\u0000\u0000\u0000\u01dd\u01df\u0005.\u0000\u0000\u01de\u01e0" + "\u0007\u0018\u0000\u0000\u01df\u01de\u0001\u0000\u0000\u0000\u01e0\u01e1" + "\u0001\u0000\u0000\u0000\u01e1\u01df\u0001\u0000\u0000\u0000\u01e1\u01e2" + "\u0001\u0000\u0000\u0000\u01e2~\u0001\u0000\u0000\u0000\u01e3\u01e5\u0007" + "\u0018\u0000\u0000\u01e4\u01e3\u0001\u0000\u0000\u0000\u01e5\u01e6\u0001" + "\u0000\u0000\u0000\u01e6\u01e4\u0001\u0000\u0000\u0000\u01e6\u01e7\u0001" + "\u0000\u0000\u0000\u01e7\u0080\u0001\u0000\u0000\u0000\u01e8\u01ea\u0007" + "\u0018\u0000\u0000\u01e9\u01e8\u0001\u0000\u0000\u0000\u01ea\u01eb\u0001" + "\u0000\u0000\u0000\u01eb\u01e9\u0001\u0000\u0000\u0000\u01eb\u01ec\u0001" + "\u0000\u0000\u0000\u01ec\u01ee\u0001\u0000\u0000\u0000\u01ed\u01ef\u0007" + "\u001a\u0000\u0000\u01ee\u01ed\u0001\u0000\u0000\u0000\u01ef\u01f0\u0001" + "\u0000\u0000\u0000\u01f0\u01ee\u0001\u0000\u0000\u0000\u01f0\u01f1\u0001" + "\u0000\u0000\u0000\u01f1\u0082\u0001\u0000\u0000\u0000\u01f2\u01f8\u0005" + "\"\u0000\u0000\u01f3\u01f7\b\u001b\u0000\u0000\u01f4\u01f5\u0005\\\u0000" + "\u0000\u01f5\u01f7\t\u0000\u0000\u0000\u01f6\u01f3\u0001\u0000\u0000\u0000" + "\u01f6\u01f4\u0001\u0000\u0000\u0000\u01f7\u01fa\u0001\u0000\u0000\u0000" + "\u01f8\u01f6\u0001\u0000\u0000\u0000\u01f8\u01f9\u0001\u0000\u0000\u0000" + "\u01f9\u01fb\u0001\u0000\u0000\u0000\u01fa\u01f8\u0001\u0000\u0000\u0000" + "\u01fb\u0207\u0005\"\u0000\u0000\u01fc\u0202\u0005\'\u0000\u0000\u01fd" + "\u0201\b\u001c\u0000\u0000\u01fe\u01ff\u0005\\\u0000\u0000\u01ff\u0201" + "\t\u0000\u0000\u0000\u0200\u01fd\u0001\u0000\u0000\u0000\u0200\u01fe\u0001" + "\u0000\u0000\u0000\u0201\u0204\u0001\u0000\u0000\u0000\u0202\u0200\u0001" + "\u0000\u0000\u0000\u0202\u0203\u0001\u0000\u0000\u0000\u0203\u0205\u0001" + "\u0000\u0000\u0000\u0204\u0202\u0001\u0000\u0000\u0000\u0205\u0207\u0005" + "\'\u0000\u0000\u0206\u01f2\u0001\u0000\u0000\u0000\u0206\u01fc\u0001\u0000" + "\u0000\u0000\u0207\u0084\u0001\u0000\u0000\u0000\u0208\u020c\u0007\u001d" + "\u0000\u0000\u0209\u020b\u0007\u001e\u0000\u0000\u020a\u0209\u0001\u0000" + "\u0000\u0000\u020b\u020e\u0001\u0000\u0000\u0000\u020c\u020a\u0001\u0000" + "\u0000\u0000\u020c\u020d\u0001\u0000\u0000\u0000\u020d\u0086\u0001\u0000" + "\u0000\u0000\u020e\u020c\u0001\u0000\u0000\u0000\u020f\u0211\u0007\u001f" + "\u0000\u0000\u0210\u020f\u0001\u0000\u0000\u0000\u0211\u0212\u0001\u0000" + "\u0000\u0000\u0212\u0210\u0001\u0000\u0000\u0000\u0212\u0213\u0001\u0000" + "\u0000\u0000\u0213\u0214\u0001\u0000\u0000\u0000\u0214\u0215\u0006C\u0000" + "\u0000\u0215\u0088\u0001\u0000\u0000\u0000\u0216\u0217\u0005/\u0000\u0000" + "\u0217\u0218\u0005/\u0000\u0000\u0218\u021c\u0001\u0000\u0000\u0000\u0219" + "\u021b\b \u0000\u0000\u021a\u0219\u0001\u0000\u0000\u0000\u021b\u021e" + "\u0001\u0000\u0000\u0000\u021c\u021a\u0001\u0000\u0000\u0000\u021c\u021d" + "\u0001\u0000\u0000\u0000\u021d\u021f\u0001\u0000\u0000\u0000\u021e\u021c" + "\u0001\u0000\u0000\u0000\u021f\u0220\u0006D\u0001\u0000\u0220\u008a\u0001" + "\u0000\u0000\u0000\u0221\u0222\u0005/\u0000\u0000\u0222\u0223\u0005*\u0000" + "\u0000\u0223\u0227\u0001\u0000\u0000\u0000\u0224\u0226\t\u0000\u0000\u0000" + "\u0225\u0224\u0001\u0000\u0000\u0000\u0226\u0229\u0001\u0000\u0000\u0000" + "\u0227\u0228\u0001\u0000\u0000\u0000\u0227\u0225\u0001\u0000\u0000\u0000" + "\u0228\u022a\u0001\u0000\u0000\u0000\u0229\u0227\u0001\u0000\u0000\u0000" + "\u022a\u022b\u0005*\u0000\u0000\u022b\u022c\u0005/\u0000\u0000\u022c\u022d" + "\u0001\u0000\u0000\u0000\u022d\u022e\u0006E\u0001\u0000\u022e\u008c\u0001" + "\u0000\u0000\u0000\u0015\u0000\u01a4\u01c5\u01cb\u01cd\u01d1\u01d6\u01db" + "\u01e1\u01e6\u01eb\u01f0\u01f6\u01f8\u0200\u0202\u0206\u020c\u0212\u021c" + "\u0227\u0002\u0000\u0001\u0000\u0006\u0000\u0000"; + public static final ATN _ATN = new ATNDeserializer().deserialize(_serializedATN.toCharArray()); + + static { + _decisionToDFA = new DFA[_ATN.getNumberOfDecisions()]; + for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) { + _decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i); + } + } +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionParser.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionParser.java new file mode 100644 index 0000000..d290fac --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionParser.java @@ -0,0 +1,3353 @@ +/* + * 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. + */ + +// Generated from AlertExpression.g4 by ANTLR 4.13.2 + +package org.apache.hertzbeat.alert.expr; + +import org.antlr.v4.runtime.FailedPredicateException; +import org.antlr.v4.runtime.NoViableAltException; +import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.RuleContext; +import org.antlr.v4.runtime.RuntimeMetaData; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; +import org.antlr.v4.runtime.Vocabulary; +import org.antlr.v4.runtime.VocabularyImpl; +import org.antlr.v4.runtime.atn.ATN; +import org.antlr.v4.runtime.atn.ATNDeserializer; +import org.antlr.v4.runtime.atn.ParserATNSimulator; +import org.antlr.v4.runtime.atn.PredictionContextCache; +import org.antlr.v4.runtime.dfa.DFA; +import org.antlr.v4.runtime.tree.ParseTreeVisitor; +import org.antlr.v4.runtime.tree.TerminalNode; + +import java.util.List; + +@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue", "this-escape"}) +public class AlertExpressionParser extends Parser { + static { + RuntimeMetaData.checkVersion("4.13.2", RuntimeMetaData.VERSION); + } + + protected static final DFA[] _decisionToDFA; + protected static final PredictionContextCache _sharedContextCache = new PredictionContextCache(); + public static final int AND = 1, OR = 2, UNLESS = 3, NOT = 4, SELECT = 5, FROM = 6, WHERE = 7, GROUP = 8, BY = 9, HAVING = 10, ORDER = 11, LIMIT = 12, OFFSET = 13, AS = 14, ASC = 15, DESC = 16, IN = 17, IS = 18, NULL = 19, LIKE = 20, BETWEEN = 21, STAR = 22, COUNT = 23, SUM = 24, AVG = 25, MIN = 26, MAX = 27, STDDEV = 28, STDVAR = 29, VARIANCE = 30, RATE_FUNCTION = 31, INCREASE_FUNCTION = 32, HISTOGRAM_QUANTILE_FUNCTION = 33, TOPK = 34, BOTTOMK = 35, QUANTILE = 36, BY_FUNCTION = 37, WITHOUT_FUNCTION = 38, GROUP_LEFT_FUNCTION = 39, GROUP_RIGHT_FUNCTION = 40, IGNORING_FUNCTION = 41, ON_FUNCTION = 42, SQL_FUNCTION = 43, PROMQL_FUNCTION = 44, GT = 45, GE = 46, LT = 47, LE = 48, EQ = 49, NE = 50, BOOL = 51, LPAREN = 52, RPAREN = 53, LBRACE = 54, RBRACE = 55, LBRACKET = 56, RBRACKET = 57, COMMA = 58, DOT = 59, COLON = 60, SEMICOLON = 61, SCIENTIFIC_NUMBER = 62, FLOAT = 63, NUMBER = 64, DURATION = 65, STRING = 66, IDENTIFIER = 67, WS = 68, LINE_COMMENT = 69, BLOCK_COMMENT = 70; + public static final int RULE_expression = 0, RULE_expr = 1, RULE_functionCall = 2, RULE_parameterList = 3, RULE_parameter = 4, RULE_number = 5, RULE_string = 6, RULE_duration = 7, RULE_functionName = 8, RULE_selectSql = 9, RULE_selectFieldList = 10, RULE_selectField = 11, RULE_groupByList = 12, RULE_orderByList = 13, RULE_orderByField = 14, RULE_limitClause = 15, RULE_relList = 16, RULE_relation = 17, RULE_conditionList = 18, RULE_compOp = 19, RULE_condition = 20, RULE_conditionUnit = 21, RULE_promql = 22, RULE_metricSelector = 23, RULE_labelMatcherList = 24, RULE_labelMatcherItem = 25, RULE_labelMatcherOp = 26, RULE_labelList = 27, RULE_instantVectorOp = 28, RULE_aggregationOperator = 29; + + private static String[] makeRuleNames() { + return new String[]{"expression", "expr", "functionCall", "parameterList", "parameter", "number", "string", "duration", "functionName", "selectSql", "selectFieldList", "selectField", "groupByList", "orderByList", "orderByField", "limitClause", "relList", "relation", "conditionList", "compOp", "condition", "conditionUnit", "promql", "metricSelector", "labelMatcherList", "labelMatcherItem", "labelMatcherOp", "labelList", "instantVectorOp", "aggregationOperator"}; + } + + public static final String[] ruleNames = makeRuleNames(); + + private static String[] makeLiteralNames() { + return new String[]{null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "'*'", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "'>'", "'>='", "'<'", "'<='", null, "'!='", "'bool'", "'('", "')'", "'{'", "'}'", "'['", "']'", "','", "'.'", "':'", "';'"}; + } + + private static final String[] _LITERAL_NAMES = makeLiteralNames(); + + private static String[] makeSymbolicNames() { + return new String[]{null, "AND", "OR", "UNLESS", "NOT", "SELECT", "FROM", "WHERE", "GROUP", "BY", "HAVING", "ORDER", "LIMIT", "OFFSET", "AS", "ASC", "DESC", "IN", "IS", "NULL", "LIKE", "BETWEEN", "STAR", "COUNT", "SUM", "AVG", "MIN", "MAX", "STDDEV", "STDVAR", "VARIANCE", "RATE_FUNCTION", "INCREASE_FUNCTION", "HISTOGRAM_QUANTILE_FUNCTION", "TOPK", "BOTTOMK", "QUANTILE", "BY_FUNCTION", "WITHOUT_FUNCTION", "GROUP_LEFT_FUNCTION", "GROUP_RIGHT_FUNCTION", "IGNORING_FUNCTION", "ON_FUNCTION", "SQL_FUNCTION", "PROMQL_FUNCTION", "GT", "GE", "LT", "LE", "EQ", "NE", "BOOL", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET", "RBRACKET", "COMMA", "DOT", "COLON", "SEMICOLON", "SCIENTIFIC_NUMBER", "FLOAT", "NUMBER", "DURATION", "STRING", "IDENTIFIER", "WS", "LINE_COMMENT", "BLOCK_COMMENT"}; + } + + private static final String[] _SYMBOLIC_NAMES = makeSymbolicNames(); + public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES); + + /** + * @deprecated Use {@link #VOCABULARY} instead. + */ + @Deprecated + public static final String[] tokenNames; + + static { + tokenNames = new String[_SYMBOLIC_NAMES.length]; + for (int i = 0; i < tokenNames.length; i++) { + tokenNames[i] = VOCABULARY.getLiteralName(i); + if (tokenNames[i] == null) { + tokenNames[i] = VOCABULARY.getSymbolicName(i); + } + + if (tokenNames[i] == null) { + tokenNames[i] = "<INVALID>"; + } + } + } + + @Override + @Deprecated + public String[] getTokenNames() { + return tokenNames; + } + + @Override + + public Vocabulary getVocabulary() { + return VOCABULARY; + } + + @Override + public String getGrammarFileName() { + return "AlertExpression.g4"; + } + + @Override + public String[] getRuleNames() { + return ruleNames; + } + + @Override + public String getSerializedATN() { + return _serializedATN; + } + + @Override + public ATN getATN() { + return _ATN; + } + + public AlertExpressionParser(TokenStream input) { + super(input); + _interp = new ParserATNSimulator(this, _ATN, _decisionToDFA, _sharedContextCache); + } + + @SuppressWarnings("CheckReturnValue") + public static class ExpressionContext extends ParserRuleContext { + public ExprContext expr() { + return getRuleContext(ExprContext.class, 0); + } + + public TerminalNode EOF() { + return getToken(AlertExpressionParser.EOF, 0); + } + + public ExpressionContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_expression; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitExpression(this); + else return visitor.visitChildren(this); + } + } + + public final ExpressionContext expression() throws RecognitionException { + ExpressionContext _localctx = new ExpressionContext(_ctx, getState()); + enterRule(_localctx, 0, RULE_expression); + try { + enterOuterAlt(_localctx, 1); + { + setState(60); + expr(0); + setState(61); + match(EOF); + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class ExprContext extends ParserRuleContext { + public ExprContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_expr; + } + + public ExprContext() { + } + + public void copyFrom(ExprContext ctx) { + super.copyFrom(ctx); + } + } + + @SuppressWarnings("CheckReturnValue") + public static class AndExprContext extends ExprContext { + public ExprContext left; + public ExprContext right; + + public TerminalNode AND() { + return getToken(AlertExpressionParser.AND, 0); + } + + public List<ExprContext> expr() { + return getRuleContexts(ExprContext.class); + } + + public ExprContext expr(int i) { + return getRuleContext(ExprContext.class, i); + } + + public AndExprContext(ExprContext ctx) { + copyFrom(ctx); + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitAndExpr(this); + else return visitor.visitChildren(this); + } + } + + @SuppressWarnings("CheckReturnValue") + public static class ComparisonExprContext extends ExprContext { + public ExprContext left; + public Token op; + public ExprContext right; + + public List<ExprContext> expr() { + return getRuleContexts(ExprContext.class); + } + + public ExprContext expr(int i) { + return getRuleContext(ExprContext.class, i); + } + + public TerminalNode GE() { + return getToken(AlertExpressionParser.GE, 0); + } + + public TerminalNode LE() { + return getToken(AlertExpressionParser.LE, 0); + } + + public TerminalNode GT() { + return getToken(AlertExpressionParser.GT, 0); + } + + public TerminalNode LT() { + return getToken(AlertExpressionParser.LT, 0); + } + + public TerminalNode EQ() { + return getToken(AlertExpressionParser.EQ, 0); + } + + public TerminalNode NE() { + return getToken(AlertExpressionParser.NE, 0); + } + + public TerminalNode BOOL() { + return getToken(AlertExpressionParser.BOOL, 0); + } + + public ComparisonExprContext(ExprContext ctx) { + copyFrom(ctx); + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitComparisonExpr(this); + else return visitor.visitChildren(this); + } + } + + @SuppressWarnings("CheckReturnValue") + public static class UnlessExprContext extends ExprContext { + public ExprContext left; + public ExprContext right; + + public TerminalNode UNLESS() { + return getToken(AlertExpressionParser.UNLESS, 0); + } + + public List<ExprContext> expr() { + return getRuleContexts(ExprContext.class); + } + + public ExprContext expr(int i) { + return getRuleContext(ExprContext.class, i); + } + + public UnlessExprContext(ExprContext ctx) { + copyFrom(ctx); + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitUnlessExpr(this); + else return visitor.visitChildren(this); + } + } + + @SuppressWarnings("CheckReturnValue") + public static class SqlExprContext extends ExprContext { + public SelectSqlContext selectSql() { + return getRuleContext(SelectSqlContext.class, 0); + } + + public SqlExprContext(ExprContext ctx) { + copyFrom(ctx); + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitSqlExpr(this); + else return visitor.visitChildren(this); + } + } + + @SuppressWarnings("CheckReturnValue") + public static class SqlCallExprContext extends ExprContext { + public TerminalNode SQL_FUNCTION() { + return getToken(AlertExpressionParser.SQL_FUNCTION, 0); + } + + public TerminalNode LPAREN() { + return getToken(AlertExpressionParser.LPAREN, 0); + } + + public StringContext string() { + return getRuleContext(StringContext.class, 0); + } + + public TerminalNode RPAREN() { + return getToken(AlertExpressionParser.RPAREN, 0); + } + + public SqlCallExprContext(ExprContext ctx) { + copyFrom(ctx); + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitSqlCallExpr(this); + else return visitor.visitChildren(this); + } + } + + @SuppressWarnings("CheckReturnValue") + public static class LiteralExprContext extends ExprContext { + public NumberContext number() { + return getRuleContext(NumberContext.class, 0); + } + + public LiteralExprContext(ExprContext ctx) { + copyFrom(ctx); + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitLiteralExpr(this); + else return visitor.visitChildren(this); + } + } + + @SuppressWarnings("CheckReturnValue") + public static class ParenExprContext extends ExprContext { + public TerminalNode LPAREN() { + return getToken(AlertExpressionParser.LPAREN, 0); + } + + public ExprContext expr() { + return getRuleContext(ExprContext.class, 0); + } + + public TerminalNode RPAREN() { + return getToken(AlertExpressionParser.RPAREN, 0); + } + + public ParenExprContext(ExprContext ctx) { + copyFrom(ctx); + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitParenExpr(this); + else return visitor.visitChildren(this); + } + } + + @SuppressWarnings("CheckReturnValue") + public static class PromqlCallExprContext extends ExprContext { + public TerminalNode PROMQL_FUNCTION() { + return getToken(AlertExpressionParser.PROMQL_FUNCTION, 0); + } + + public TerminalNode LPAREN() { + return getToken(AlertExpressionParser.LPAREN, 0); + } + + public StringContext string() { + return getRuleContext(StringContext.class, 0); + } + + public TerminalNode RPAREN() { + return getToken(AlertExpressionParser.RPAREN, 0); + } + + public PromqlCallExprContext(ExprContext ctx) { + copyFrom(ctx); + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitPromqlCallExpr(this); + else return visitor.visitChildren(this); + } + } + + @SuppressWarnings("CheckReturnValue") + public static class PromqlExprContext extends ExprContext { + public PromqlContext promql() { + return getRuleContext(PromqlContext.class, 0); + } + + public PromqlExprContext(ExprContext ctx) { + copyFrom(ctx); + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitPromqlExpr(this); + else return visitor.visitChildren(this); + } + } + + @SuppressWarnings("CheckReturnValue") + public static class OrExprContext extends ExprContext { + public ExprContext left; + public ExprContext right; + + public TerminalNode OR() { + return getToken(AlertExpressionParser.OR, 0); + } + + public List<ExprContext> expr() { + return getRuleContexts(ExprContext.class); + } + + public ExprContext expr(int i) { + return getRuleContext(ExprContext.class, i); + } + + public OrExprContext(ExprContext ctx) { + copyFrom(ctx); + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitOrExpr(this); + else return visitor.visitChildren(this); + } + } + + public final ExprContext expr() throws RecognitionException { + return expr(0); + } + + private ExprContext expr(int _p) throws RecognitionException { + ParserRuleContext _parentctx = _ctx; + int _parentState = getState(); + ExprContext _localctx = new ExprContext(_ctx, _parentState); + ExprContext _prevctx = _localctx; + int _startState = 2; + enterRecursionRule(_localctx, 2, RULE_expr, _p); + int _la; + try { + int _alt; + enterOuterAlt(_localctx, 1); + { + setState(81); + _errHandler.sync(this); + switch (_input.LA(1)) { + case LPAREN: { + _localctx = new ParenExprContext(_localctx); + _ctx = _localctx; + _prevctx = _localctx; + + setState(64); + match(LPAREN); + setState(65); + expr(0); + setState(66); + match(RPAREN); + } + break; + case COUNT: + case SUM: + case AVG: + case MIN: + case MAX: + case STDDEV: + case STDVAR: + case RATE_FUNCTION: + case INCREASE_FUNCTION: + case HISTOGRAM_QUANTILE_FUNCTION: + case TOPK: + case BOTTOMK: + case QUANTILE: + case BY_FUNCTION: + case WITHOUT_FUNCTION: + case GROUP_LEFT_FUNCTION: + case GROUP_RIGHT_FUNCTION: + case IGNORING_FUNCTION: + case ON_FUNCTION: + case LBRACE: + case IDENTIFIER: { + _localctx = new PromqlExprContext(_localctx); + _ctx = _localctx; + _prevctx = _localctx; + setState(68); + promql(0); + } + break; + case SELECT: { + _localctx = new SqlExprContext(_localctx); + _ctx = _localctx; + _prevctx = _localctx; + setState(69); + selectSql(); + } + break; + case SCIENTIFIC_NUMBER: + case FLOAT: + case NUMBER: { + _localctx = new LiteralExprContext(_localctx); + _ctx = _localctx; + _prevctx = _localctx; + setState(70); + number(); + } + break; + case SQL_FUNCTION: { + _localctx = new SqlCallExprContext(_localctx); + _ctx = _localctx; + _prevctx = _localctx; + setState(71); + match(SQL_FUNCTION); + setState(72); + match(LPAREN); + setState(73); + string(); + setState(74); + match(RPAREN); + } + break; + case PROMQL_FUNCTION: { + _localctx = new PromqlCallExprContext(_localctx); + _ctx = _localctx; + _prevctx = _localctx; + setState(76); + match(PROMQL_FUNCTION); + setState(77); + match(LPAREN); + setState(78); + string(); + setState(79); + match(RPAREN); + } + break; + default: + throw new NoViableAltException(this); + } + _ctx.stop = _input.LT(-1); + setState(100); + _errHandler.sync(this); + _alt = getInterpreter().adaptivePredict(_input, 3, _ctx); + while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER) { + if (_alt == 1) { + if (_parseListeners != null) triggerExitRuleEvent(); + _prevctx = _localctx; + { + setState(98); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 2, _ctx)) { + case 1: { + _localctx = new ComparisonExprContext(new ExprContext(_parentctx, _parentState)); + ((ComparisonExprContext) _localctx).left = _prevctx; + pushNewRecursionContext(_localctx, _startState, RULE_expr); + setState(83); + if (!(precpred(_ctx, 9))) + throw new FailedPredicateException(this, "precpred(_ctx, 9)"); + setState(84); + ((ComparisonExprContext) _localctx).op = _input.LT(1); + _la = _input.LA(1); + if (!((((_la) & ~0x3f) == 0 && ((1L << _la) & 2216615441596416L) != 0))) { + ((ComparisonExprContext) _localctx).op = (Token) _errHandler.recoverInline(this); + } else { + if (_input.LA(1) == Token.EOF) matchedEOF = true; + _errHandler.reportMatch(this); + consume(); + } + setState(86); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la == BOOL) { + { + setState(85); + match(BOOL); + } + } + + setState(88); + ((ComparisonExprContext) _localctx).right = expr(10); + } + break; + case 2: { + _localctx = new AndExprContext(new ExprContext(_parentctx, _parentState)); + ((AndExprContext) _localctx).left = _prevctx; + pushNewRecursionContext(_localctx, _startState, RULE_expr); + setState(89); + if (!(precpred(_ctx, 8))) + throw new FailedPredicateException(this, "precpred(_ctx, 8)"); + setState(90); + match(AND); + setState(91); + ((AndExprContext) _localctx).right = expr(9); + } + break; + case 3: { + _localctx = new UnlessExprContext(new ExprContext(_parentctx, _parentState)); + ((UnlessExprContext) _localctx).left = _prevctx; + pushNewRecursionContext(_localctx, _startState, RULE_expr); + setState(92); + if (!(precpred(_ctx, 7))) + throw new FailedPredicateException(this, "precpred(_ctx, 7)"); + setState(93); + match(UNLESS); + setState(94); + ((UnlessExprContext) _localctx).right = expr(8); + } + break; + case 4: { + _localctx = new OrExprContext(new ExprContext(_parentctx, _parentState)); + ((OrExprContext) _localctx).left = _prevctx; + pushNewRecursionContext(_localctx, _startState, RULE_expr); + setState(95); + if (!(precpred(_ctx, 6))) + throw new FailedPredicateException(this, "precpred(_ctx, 6)"); + setState(96); + match(OR); + setState(97); + ((OrExprContext) _localctx).right = expr(7); + } + break; + } + } + } + setState(102); + _errHandler.sync(this); + _alt = getInterpreter().adaptivePredict(_input, 3, _ctx); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + unrollRecursionContexts(_parentctx); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class FunctionCallContext extends ParserRuleContext { + public FunctionNameContext functionName() { + return getRuleContext(FunctionNameContext.class, 0); + } + + public TerminalNode LPAREN() { + return getToken(AlertExpressionParser.LPAREN, 0); + } + + public ParameterListContext parameterList() { + return getRuleContext(ParameterListContext.class, 0); + } + + public TerminalNode RPAREN() { + return getToken(AlertExpressionParser.RPAREN, 0); + } + + public FunctionCallContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_functionCall; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitFunctionCall(this); + else return visitor.visitChildren(this); + } + } + + public final FunctionCallContext functionCall() throws RecognitionException { + FunctionCallContext _localctx = new FunctionCallContext(_ctx, getState()); + enterRule(_localctx, 4, RULE_functionCall); + try { + enterOuterAlt(_localctx, 1); + { + setState(103); + functionName(); + setState(104); + match(LPAREN); + setState(105); + parameterList(); + setState(106); + match(RPAREN); + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class ParameterListContext extends ParserRuleContext { + public List<ParameterContext> parameter() { + return getRuleContexts(ParameterContext.class); + } + + public ParameterContext parameter(int i) { + return getRuleContext(ParameterContext.class, i); + } + + public List<TerminalNode> COMMA() { + return getTokens(AlertExpressionParser.COMMA); + } + + public TerminalNode COMMA(int i) { + return getToken(AlertExpressionParser.COMMA, i); + } + + public ParameterListContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_parameterList; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitParameterList(this); + else return visitor.visitChildren(this); + } + } + + public final ParameterListContext parameterList() throws RecognitionException { + ParameterListContext _localctx = new ParameterListContext(_ctx, getState()); + enterRule(_localctx, 6, RULE_parameterList); + int _la; + try { + enterOuterAlt(_localctx, 1); + { + setState(108); + parameter(); + setState(113); + _errHandler.sync(this); + _la = _input.LA(1); + while (_la == COMMA) { + { + { + setState(109); + match(COMMA); + setState(110); + parameter(); + } + } + setState(115); + _errHandler.sync(this); + _la = _input.LA(1); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class ParameterContext extends ParserRuleContext { + public ExprContext expr() { + return getRuleContext(ExprContext.class, 0); + } + + public TerminalNode STAR() { + return getToken(AlertExpressionParser.STAR, 0); + } + + public StringContext string() { + return getRuleContext(StringContext.class, 0); + } + + public DurationContext duration() { + return getRuleContext(DurationContext.class, 0); + } + + public ParameterContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_parameter; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitParameter(this); + else return visitor.visitChildren(this); + } + } + + public final ParameterContext parameter() throws RecognitionException { + ParameterContext _localctx = new ParameterContext(_ctx, getState()); + enterRule(_localctx, 8, RULE_parameter); + try { + setState(120); + _errHandler.sync(this); + switch (_input.LA(1)) { + case SELECT: + case COUNT: + case SUM: + case AVG: + case MIN: + case MAX: + case STDDEV: + case STDVAR: + case RATE_FUNCTION: + case INCREASE_FUNCTION: + case HISTOGRAM_QUANTILE_FUNCTION: + case TOPK: + case BOTTOMK: + case QUANTILE: + case BY_FUNCTION: + case WITHOUT_FUNCTION: + case GROUP_LEFT_FUNCTION: + case GROUP_RIGHT_FUNCTION: + case IGNORING_FUNCTION: + case ON_FUNCTION: + case SQL_FUNCTION: + case PROMQL_FUNCTION: + case LPAREN: + case LBRACE: + case SCIENTIFIC_NUMBER: + case FLOAT: + case NUMBER: + case IDENTIFIER: + enterOuterAlt(_localctx, 1); + { + setState(116); + expr(0); + } + break; + case STAR: + enterOuterAlt(_localctx, 2); + { + setState(117); + match(STAR); + } + break; + case STRING: + enterOuterAlt(_localctx, 3); + { + setState(118); + string(); + } + break; + case DURATION: + enterOuterAlt(_localctx, 4); + { + setState(119); + duration(); + } + break; + default: + throw new NoViableAltException(this); + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class NumberContext extends ParserRuleContext { + public TerminalNode NUMBER() { + return getToken(AlertExpressionParser.NUMBER, 0); + } + + public TerminalNode FLOAT() { + return getToken(AlertExpressionParser.FLOAT, 0); + } + + public TerminalNode SCIENTIFIC_NUMBER() { + return getToken(AlertExpressionParser.SCIENTIFIC_NUMBER, 0); + } + + public NumberContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_number; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitNumber(this); + else return visitor.visitChildren(this); + } + } + + public final NumberContext number() throws RecognitionException { + NumberContext _localctx = new NumberContext(_ctx, getState()); + enterRule(_localctx, 10, RULE_number); + int _la; + try { + enterOuterAlt(_localctx, 1); + { + setState(122); + _la = _input.LA(1); + if (!(((((_la - 62)) & ~0x3f) == 0 && ((1L << (_la - 62)) & 7L) != 0))) { + _errHandler.recoverInline(this); + } else { + if (_input.LA(1) == Token.EOF) matchedEOF = true; + _errHandler.reportMatch(this); + consume(); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class StringContext extends ParserRuleContext { + public TerminalNode STRING() { + return getToken(AlertExpressionParser.STRING, 0); + } + + public StringContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_string; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitString(this); + else return visitor.visitChildren(this); + } + } + + public final StringContext string() throws RecognitionException { + StringContext _localctx = new StringContext(_ctx, getState()); + enterRule(_localctx, 12, RULE_string); + try { + enterOuterAlt(_localctx, 1); + { + setState(124); + match(STRING); + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class DurationContext extends ParserRuleContext { + public TerminalNode DURATION() { + return getToken(AlertExpressionParser.DURATION, 0); + } + + public DurationContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_duration; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitDuration(this); + else return visitor.visitChildren(this); + } + } + + public final DurationContext duration() throws RecognitionException { + DurationContext _localctx = new DurationContext(_ctx, getState()); + enterRule(_localctx, 14, RULE_duration); + try { + enterOuterAlt(_localctx, 1); + { + setState(126); + match(DURATION); + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class FunctionNameContext extends ParserRuleContext { + public TerminalNode COUNT() { + return getToken(AlertExpressionParser.COUNT, 0); + } + + public TerminalNode AVG() { + return getToken(AlertExpressionParser.AVG, 0); + } + + public TerminalNode SUM() { + return getToken(AlertExpressionParser.SUM, 0); + } + + public TerminalNode MIN() { + return getToken(AlertExpressionParser.MIN, 0); + } + + public TerminalNode MAX() { + return getToken(AlertExpressionParser.MAX, 0); + } + + public TerminalNode RATE_FUNCTION() { + return getToken(AlertExpressionParser.RATE_FUNCTION, 0); + } + + public TerminalNode INCREASE_FUNCTION() { + return getToken(AlertExpressionParser.INCREASE_FUNCTION, 0); + } + + public TerminalNode HISTOGRAM_QUANTILE_FUNCTION() { + return getToken(AlertExpressionParser.HISTOGRAM_QUANTILE_FUNCTION, 0); + } + + public TerminalNode BY_FUNCTION() { + return getToken(AlertExpressionParser.BY_FUNCTION, 0); + } + + public TerminalNode WITHOUT_FUNCTION() { + return getToken(AlertExpressionParser.WITHOUT_FUNCTION, 0); + } + + public TerminalNode GROUP_LEFT_FUNCTION() { + return getToken(AlertExpressionParser.GROUP_LEFT_FUNCTION, 0); + } + + public TerminalNode GROUP_RIGHT_FUNCTION() { + return getToken(AlertExpressionParser.GROUP_RIGHT_FUNCTION, 0); + } + + public TerminalNode IGNORING_FUNCTION() { + return getToken(AlertExpressionParser.IGNORING_FUNCTION, 0); + } + + public TerminalNode ON_FUNCTION() { + return getToken(AlertExpressionParser.ON_FUNCTION, 0); + } + + public TerminalNode IDENTIFIER() { + return getToken(AlertExpressionParser.IDENTIFIER, 0); + } + + public FunctionNameContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_functionName; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitFunctionName(this); + else return visitor.visitChildren(this); + } + } + + public final FunctionNameContext functionName() throws RecognitionException { + FunctionNameContext _localctx = new FunctionNameContext(_ctx, getState()); + enterRule(_localctx, 16, RULE_functionName); + int _la; + try { + enterOuterAlt(_localctx, 1); + { + setState(128); + _la = _input.LA(1); + if (!(((((_la - 23)) & ~0x3f) == 0 && ((1L << (_la - 23)) & 17592187078431L) != 0))) { + _errHandler.recoverInline(this); + } else { + if (_input.LA(1) == Token.EOF) matchedEOF = true; + _errHandler.reportMatch(this); + consume(); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class SelectSqlContext extends ParserRuleContext { + public TerminalNode SELECT() { + return getToken(AlertExpressionParser.SELECT, 0); + } + + public SelectFieldListContext selectFieldList() { + return getRuleContext(SelectFieldListContext.class, 0); + } + + public TerminalNode FROM() { + return getToken(AlertExpressionParser.FROM, 0); + } + + public RelListContext relList() { + return getRuleContext(RelListContext.class, 0); + } + + public TerminalNode WHERE() { + return getToken(AlertExpressionParser.WHERE, 0); + } + + public List<ConditionListContext> conditionList() { + return getRuleContexts(ConditionListContext.class); + } + + public ConditionListContext conditionList(int i) { + return getRuleContext(ConditionListContext.class, i); + } + + public TerminalNode GROUP() { + return getToken(AlertExpressionParser.GROUP, 0); + } + + public List<TerminalNode> BY() { + return getTokens(AlertExpressionParser.BY); + } + + public TerminalNode BY(int i) { + return getToken(AlertExpressionParser.BY, i); + } + + public GroupByListContext groupByList() { + return getRuleContext(GroupByListContext.class, 0); + } + + public TerminalNode HAVING() { + return getToken(AlertExpressionParser.HAVING, 0); + } + + public TerminalNode ORDER() { + return getToken(AlertExpressionParser.ORDER, 0); + } + + public OrderByListContext orderByList() { + return getRuleContext(OrderByListContext.class, 0); + } + + public TerminalNode LIMIT() { + return getToken(AlertExpressionParser.LIMIT, 0); + } + + public LimitClauseContext limitClause() { + return getRuleContext(LimitClauseContext.class, 0); + } + + public SelectSqlContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_selectSql; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitSelectSql(this); + else return visitor.visitChildren(this); + } + } + + public final SelectSqlContext selectSql() throws RecognitionException { + SelectSqlContext _localctx = new SelectSqlContext(_ctx, getState()); + enterRule(_localctx, 18, RULE_selectSql); + try { + enterOuterAlt(_localctx, 1); + { + setState(130); + match(SELECT); + setState(131); + selectFieldList(); + setState(132); + match(FROM); + setState(133); + relList(); + setState(136); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 6, _ctx)) { + case 1: { + setState(134); + match(WHERE); + setState(135); + conditionList(0); + } + break; + } + setState(141); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 7, _ctx)) { + case 1: { + setState(138); + match(GROUP); + setState(139); + match(BY); + setState(140); + groupByList(); + } + break; + } + setState(145); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 8, _ctx)) { + case 1: { + setState(143); + match(HAVING); + setState(144); + conditionList(0); + } + break; + } + setState(150); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 9, _ctx)) { + case 1: { + setState(147); + match(ORDER); + setState(148); + match(BY); + setState(149); + orderByList(); + } + break; + } + setState(154); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 10, _ctx)) { + case 1: { + setState(152); + match(LIMIT); + setState(153); + limitClause(); + } + break; + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class SelectFieldListContext extends ParserRuleContext { + public List<SelectFieldContext> selectField() { + return getRuleContexts(SelectFieldContext.class); + } + + public SelectFieldContext selectField(int i) { + return getRuleContext(SelectFieldContext.class, i); + } + + public List<TerminalNode> COMMA() { + return getTokens(AlertExpressionParser.COMMA); + } + + public TerminalNode COMMA(int i) { + return getToken(AlertExpressionParser.COMMA, i); + } + + public SelectFieldListContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_selectFieldList; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitSelectFieldList(this); + else return visitor.visitChildren(this); + } + } + + public final SelectFieldListContext selectFieldList() throws RecognitionException { + SelectFieldListContext _localctx = new SelectFieldListContext(_ctx, getState()); + enterRule(_localctx, 20, RULE_selectFieldList); + int _la; + try { + enterOuterAlt(_localctx, 1); + { + setState(156); + selectField(); + setState(161); + _errHandler.sync(this); + _la = _input.LA(1); + while (_la == COMMA) { + { + { + setState(157); + match(COMMA); + setState(158); + selectField(); + } + } + setState(163); + _errHandler.sync(this); + _la = _input.LA(1); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class SelectFieldContext extends ParserRuleContext { + public FunctionCallContext functionCall() { + return getRuleContext(FunctionCallContext.class, 0); + } + + public List<TerminalNode> IDENTIFIER() { + return getTokens(AlertExpressionParser.IDENTIFIER); + } + + public TerminalNode IDENTIFIER(int i) { + return getToken(AlertExpressionParser.IDENTIFIER, i); + } + + public TerminalNode AS() { + return getToken(AlertExpressionParser.AS, 0); + } + + public TerminalNode STAR() { + return getToken(AlertExpressionParser.STAR, 0); + } + + public TerminalNode DOT() { + return getToken(AlertExpressionParser.DOT, 0); + } + + public SelectFieldContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_selectField; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitSelectField(this); + else return visitor.visitChildren(this); + } + } + + public final SelectFieldContext selectField() throws RecognitionException { + SelectFieldContext _localctx = new SelectFieldContext(_ctx, getState()); + enterRule(_localctx, 22, RULE_selectField); + int _la; + try { + setState(194); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 20, _ctx)) { + case 1: + enterOuterAlt(_localctx, 1); + { + setState(164); + functionCall(); + setState(169); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la == AS || _la == IDENTIFIER) { + { + setState(166); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la == AS) { + { + setState(165); + match(AS); + } + } + + setState(168); + match(IDENTIFIER); + } + } + + } + break; + case 2: + enterOuterAlt(_localctx, 2); + { + setState(171); + match(IDENTIFIER); + setState(176); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la == AS || _la == IDENTIFIER) { + { + setState(173); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la == AS) { + { + setState(172); + match(AS); + } + } + + setState(175); + match(IDENTIFIER); + } + } + + } + break; + case 3: + enterOuterAlt(_localctx, 3); + { + setState(178); + match(STAR); + setState(183); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la == AS || _la == IDENTIFIER) { + { + setState(180); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la == AS) { + { + setState(179); + match(AS); + } + } + + setState(182); + match(IDENTIFIER); + } + } + + } + break; + case 4: + enterOuterAlt(_localctx, 4); + { + setState(185); + match(IDENTIFIER); + setState(186); + match(DOT); + setState(187); + match(IDENTIFIER); + setState(192); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la == AS || _la == IDENTIFIER) { + { + setState(189); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la == AS) { + { + setState(188); + match(AS); + } + } + + setState(191); + match(IDENTIFIER); + } + } + + } + break; + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class GroupByListContext extends ParserRuleContext { + public List<TerminalNode> IDENTIFIER() { + return getTokens(AlertExpressionParser.IDENTIFIER); + } + + public TerminalNode IDENTIFIER(int i) { + return getToken(AlertExpressionParser.IDENTIFIER, i); + } + + public List<TerminalNode> COMMA() { + return getTokens(AlertExpressionParser.COMMA); + } + + public TerminalNode COMMA(int i) { + return getToken(AlertExpressionParser.COMMA, i); + } + + public GroupByListContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_groupByList; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitGroupByList(this); + else return visitor.visitChildren(this); + } + } + + public final GroupByListContext groupByList() throws RecognitionException { + GroupByListContext _localctx = new GroupByListContext(_ctx, getState()); + enterRule(_localctx, 24, RULE_groupByList); + try { + int _alt; + enterOuterAlt(_localctx, 1); + { + setState(196); + match(IDENTIFIER); + setState(201); + _errHandler.sync(this); + _alt = getInterpreter().adaptivePredict(_input, 21, _ctx); + while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER) { + if (_alt == 1) { + { + { + setState(197); + match(COMMA); + setState(198); + match(IDENTIFIER); + } + } + } + setState(203); + _errHandler.sync(this); + _alt = getInterpreter().adaptivePredict(_input, 21, _ctx); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class OrderByListContext extends ParserRuleContext { + public List<OrderByFieldContext> orderByField() { + return getRuleContexts(OrderByFieldContext.class); + } + + public OrderByFieldContext orderByField(int i) { + return getRuleContext(OrderByFieldContext.class, i); + } + + public List<TerminalNode> COMMA() { + return getTokens(AlertExpressionParser.COMMA); + } + + public TerminalNode COMMA(int i) { + return getToken(AlertExpressionParser.COMMA, i); + } + + public OrderByListContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_orderByList; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitOrderByList(this); + else return visitor.visitChildren(this); + } + } + + public final OrderByListContext orderByList() throws RecognitionException { + OrderByListContext _localctx = new OrderByListContext(_ctx, getState()); + enterRule(_localctx, 26, RULE_orderByList); + try { + int _alt; + enterOuterAlt(_localctx, 1); + { + setState(204); + orderByField(); + setState(209); + _errHandler.sync(this); + _alt = getInterpreter().adaptivePredict(_input, 22, _ctx); + while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER) { + if (_alt == 1) { + { + { + setState(205); + match(COMMA); + setState(206); + orderByField(); + } + } + } + setState(211); + _errHandler.sync(this); + _alt = getInterpreter().adaptivePredict(_input, 22, _ctx); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class OrderByFieldContext extends ParserRuleContext { + public TerminalNode IDENTIFIER() { + return getToken(AlertExpressionParser.IDENTIFIER, 0); + } + + public TerminalNode ASC() { + return getToken(AlertExpressionParser.ASC, 0); + } + + public TerminalNode DESC() { + return getToken(AlertExpressionParser.DESC, 0); + } + + public FunctionCallContext functionCall() { + return getRuleContext(FunctionCallContext.class, 0); + } + + public OrderByFieldContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_orderByField; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitOrderByField(this); + else return visitor.visitChildren(this); + } + } + + public final OrderByFieldContext orderByField() throws RecognitionException { + OrderByFieldContext _localctx = new OrderByFieldContext(_ctx, getState()); + enterRule(_localctx, 28, RULE_orderByField); + int _la; + try { + setState(220); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 25, _ctx)) { + case 1: + enterOuterAlt(_localctx, 1); + { + setState(212); + match(IDENTIFIER); + setState(214); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 23, _ctx)) { + case 1: { + setState(213); + _la = _input.LA(1); + if (!(_la == ASC || _la == DESC)) { + _errHandler.recoverInline(this); + } else { + if (_input.LA(1) == Token.EOF) matchedEOF = true; + _errHandler.reportMatch(this); + consume(); + } + } + break; + } + } + break; + case 2: + enterOuterAlt(_localctx, 2); + { + setState(216); + functionCall(); + setState(218); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 24, _ctx)) { + case 1: { + setState(217); + _la = _input.LA(1); + if (!(_la == ASC || _la == DESC)) { + _errHandler.recoverInline(this); + } else { + if (_input.LA(1) == Token.EOF) matchedEOF = true; + _errHandler.reportMatch(this); + consume(); + } + } + break; + } + } + break; + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class LimitClauseContext extends ParserRuleContext { + public TerminalNode NUMBER() { + return getToken(AlertExpressionParser.NUMBER, 0); + } + + public LimitClauseContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_limitClause; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitLimitClause(this); + else return visitor.visitChildren(this); + } + } + + public final LimitClauseContext limitClause() throws RecognitionException { + LimitClauseContext _localctx = new LimitClauseContext(_ctx, getState()); + enterRule(_localctx, 30, RULE_limitClause); + try { + enterOuterAlt(_localctx, 1); + { + setState(222); + match(NUMBER); + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class RelListContext extends ParserRuleContext { + public List<RelationContext> relation() { + return getRuleContexts(RelationContext.class); + } + + public RelationContext relation(int i) { + return getRuleContext(RelationContext.class, i); + } + + public List<TerminalNode> COMMA() { + return getTokens(AlertExpressionParser.COMMA); + } + + public TerminalNode COMMA(int i) { + return getToken(AlertExpressionParser.COMMA, i); + } + + public RelListContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_relList; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitRelList(this); + else return visitor.visitChildren(this); + } + } + + public final RelListContext relList() throws RecognitionException { + RelListContext _localctx = new RelListContext(_ctx, getState()); + enterRule(_localctx, 32, RULE_relList); + try { + int _alt; + enterOuterAlt(_localctx, 1); + { + setState(224); + relation(); + setState(229); + _errHandler.sync(this); + _alt = getInterpreter().adaptivePredict(_input, 26, _ctx); + while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER) { + if (_alt == 1) { + { + { + setState(225); + match(COMMA); + setState(226); + relation(); + } + } + } + setState(231); + _errHandler.sync(this); + _alt = getInterpreter().adaptivePredict(_input, 26, _ctx); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class RelationContext extends ParserRuleContext { + public List<TerminalNode> IDENTIFIER() { + return getTokens(AlertExpressionParser.IDENTIFIER); + } + + public TerminalNode IDENTIFIER(int i) { + return getToken(AlertExpressionParser.IDENTIFIER, i); + } + + public TerminalNode AS() { + return getToken(AlertExpressionParser.AS, 0); + } + + public TerminalNode LPAREN() { + return getToken(AlertExpressionParser.LPAREN, 0); + } + + public SelectSqlContext selectSql() { + return getRuleContext(SelectSqlContext.class, 0); + } + + public TerminalNode RPAREN() { + return getToken(AlertExpressionParser.RPAREN, 0); + } + + public RelationContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_relation; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitRelation(this); + else return visitor.visitChildren(this); + } + } + + public final RelationContext relation() throws RecognitionException { + RelationContext _localctx = new RelationContext(_ctx, getState()); + enterRule(_localctx, 34, RULE_relation); + int _la; + try { + setState(245); + _errHandler.sync(this); + switch (_input.LA(1)) { + case IDENTIFIER: + enterOuterAlt(_localctx, 1); + { + setState(232); + match(IDENTIFIER); + setState(237); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 28, _ctx)) { + case 1: { + setState(234); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la == AS) { + { + setState(233); + match(AS); + } + } + + setState(236); + match(IDENTIFIER); + } + break; + } + } + break; + case LPAREN: + enterOuterAlt(_localctx, 2); + { + setState(239); + match(LPAREN); + setState(240); + selectSql(); + setState(241); + match(RPAREN); + setState(242); + match(AS); + setState(243); + match(IDENTIFIER); + } + break; + default: + throw new NoViableAltException(this); + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class ConditionListContext extends ParserRuleContext { + public ConditionContext condition() { + return getRuleContext(ConditionContext.class, 0); + } + + public TerminalNode LPAREN() { + return getToken(AlertExpressionParser.LPAREN, 0); + } + + public List<ConditionListContext> conditionList() { + return getRuleContexts(ConditionListContext.class); + } + + public ConditionListContext conditionList(int i) { + return getRuleContext(ConditionListContext.class, i); + } + + public TerminalNode RPAREN() { + return getToken(AlertExpressionParser.RPAREN, 0); + } + + public TerminalNode AND() { + return getToken(AlertExpressionParser.AND, 0); + } + + public TerminalNode OR() { + return getToken(AlertExpressionParser.OR, 0); + } + + public ConditionListContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_conditionList; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitConditionList(this); + else return visitor.visitChildren(this); + } + } + + public final ConditionListContext conditionList() throws RecognitionException { + return conditionList(0); + } + + private ConditionListContext conditionList(int _p) throws RecognitionException { + ParserRuleContext _parentctx = _ctx; + int _parentState = getState(); + ConditionListContext _localctx = new ConditionListContext(_ctx, _parentState); + ConditionListContext _prevctx = _localctx; + int _startState = 36; + enterRecursionRule(_localctx, 36, RULE_conditionList, _p); + try { + int _alt; + enterOuterAlt(_localctx, 1); + { + setState(253); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 30, _ctx)) { + case 1: { + setState(248); + condition(); + } + break; + case 2: { + setState(249); + match(LPAREN); + setState(250); + conditionList(0); + setState(251); + match(RPAREN); + } + break; + } + _ctx.stop = _input.LT(-1); + setState(263); + _errHandler.sync(this); + _alt = getInterpreter().adaptivePredict(_input, 32, _ctx); + while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER) { + if (_alt == 1) { + if (_parseListeners != null) triggerExitRuleEvent(); + _prevctx = _localctx; + { + setState(261); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 31, _ctx)) { + case 1: { + _localctx = new ConditionListContext(_parentctx, _parentState); + pushNewRecursionContext(_localctx, _startState, RULE_conditionList); + setState(255); + if (!(precpred(_ctx, 4))) + throw new FailedPredicateException(this, "precpred(_ctx, 4)"); + setState(256); + match(AND); + setState(257); + conditionList(5); + } + break; + case 2: { + _localctx = new ConditionListContext(_parentctx, _parentState); + pushNewRecursionContext(_localctx, _startState, RULE_conditionList); + setState(258); + if (!(precpred(_ctx, 3))) + throw new FailedPredicateException(this, "precpred(_ctx, 3)"); + setState(259); + match(OR); + setState(260); + conditionList(4); + } + break; + } + } + } + setState(265); + _errHandler.sync(this); + _alt = getInterpreter().adaptivePredict(_input, 32, _ctx); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + unrollRecursionContexts(_parentctx); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class CompOpContext extends ParserRuleContext { + public TerminalNode EQ() { + return getToken(AlertExpressionParser.EQ, 0); + } + + public TerminalNode LT() { + return getToken(AlertExpressionParser.LT, 0); + } + + public TerminalNode GT() { + return getToken(AlertExpressionParser.GT, 0); + } + + public TerminalNode LE() { + return getToken(AlertExpressionParser.LE, 0); + } + + public TerminalNode GE() { + return getToken(AlertExpressionParser.GE, 0); + } + + public TerminalNode NE() { + return getToken(AlertExpressionParser.NE, 0); + } + + public TerminalNode LIKE() { + return getToken(AlertExpressionParser.LIKE, 0); + } + + public TerminalNode NOT() { + return getToken(AlertExpressionParser.NOT, 0); + } + + public TerminalNode IN() { + return getToken(AlertExpressionParser.IN, 0); + } + + public TerminalNode IS() { + return getToken(AlertExpressionParser.IS, 0); + } + + public CompOpContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_compOp; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitCompOp(this); + else return visitor.visitChildren(this); + } + } + + public final CompOpContext compOp() throws RecognitionException { + CompOpContext _localctx = new CompOpContext(_ctx, getState()); + enterRule(_localctx, 38, RULE_compOp); + try { + setState(281); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 33, _ctx)) { + case 1: + enterOuterAlt(_localctx, 1); + { + setState(266); + match(EQ); + } + break; + case 2: + enterOuterAlt(_localctx, 2); + { + setState(267); + match(LT); + } + break; + case 3: + enterOuterAlt(_localctx, 3); + { + setState(268); + match(GT); + } + break; + case 4: + enterOuterAlt(_localctx, 4); + { + setState(269); + match(LE); + } + break; + case 5: + enterOuterAlt(_localctx, 5); + { + setState(270); + match(GE); + } + break; + case 6: + enterOuterAlt(_localctx, 6); + { + setState(271); + match(NE); + } + break; + case 7: + enterOuterAlt(_localctx, 7); + { + setState(272); + match(LIKE); + } + break; + case 8: + enterOuterAlt(_localctx, 8); + { + setState(273); + match(NOT); + setState(274); + match(LIKE); + } + break; + case 9: + enterOuterAlt(_localctx, 9); + { + setState(275); + match(IN); + } + break; + case 10: + enterOuterAlt(_localctx, 10); + { + setState(276); + match(NOT); + setState(277); + match(IN); + } + break; + case 11: + enterOuterAlt(_localctx, 11); + { + setState(278); + match(IS); + } + break; + case 12: + enterOuterAlt(_localctx, 12); + { + setState(279); + match(IS); + setState(280); + match(NOT); + } + break; + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class ConditionContext extends ParserRuleContext { + public List<ConditionUnitContext> conditionUnit() { + return getRuleContexts(ConditionUnitContext.class); + } + + public ConditionUnitContext conditionUnit(int i) { + return getRuleContext(ConditionUnitContext.class, i); + } + + public CompOpContext compOp() { + return getRuleContext(CompOpContext.class, 0); + } + + public TerminalNode LPAREN() { + return getToken(AlertExpressionParser.LPAREN, 0); + } + + public ConditionContext condition() { + return getRuleContext(ConditionContext.class, 0); + } + + public TerminalNode RPAREN() { + return getToken(AlertExpressionParser.RPAREN, 0); + } + + public TerminalNode IDENTIFIER() { + return getToken(AlertExpressionParser.IDENTIFIER, 0); + } + + public TerminalNode BETWEEN() { + return getToken(AlertExpressionParser.BETWEEN, 0); + } + + public List<NumberContext> number() { + return getRuleContexts(NumberContext.class); + } + + public NumberContext number(int i) { + return getRuleContext(NumberContext.class, i); + } + + public TerminalNode AND() { + return getToken(AlertExpressionParser.AND, 0); + } + + public ConditionContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_condition; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitCondition(this); + else return visitor.visitChildren(this); + } + } + + public final ConditionContext condition() throws RecognitionException { + ConditionContext _localctx = new ConditionContext(_ctx, getState()); + enterRule(_localctx, 40, RULE_condition); + try { + setState(297); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 34, _ctx)) { + case 1: + enterOuterAlt(_localctx, 1); + { + setState(283); + conditionUnit(); + setState(284); + compOp(); + setState(285); + conditionUnit(); + } + break; + case 2: + enterOuterAlt(_localctx, 2); + { + setState(287); + match(LPAREN); + setState(288); + condition(); + setState(289); + match(RPAREN); + } + break; + case 3: + enterOuterAlt(_localctx, 3); + { + setState(291); + match(IDENTIFIER); + setState(292); + match(BETWEEN); + setState(293); + number(); + setState(294); + match(AND); + setState(295); + number(); + } + break; + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class ConditionUnitContext extends ParserRuleContext { + public NumberContext number() { + return getRuleContext(NumberContext.class, 0); + } + + public StringContext string() { + return getRuleContext(StringContext.class, 0); + } + + public List<TerminalNode> IDENTIFIER() { + return getTokens(AlertExpressionParser.IDENTIFIER); + } + + public TerminalNode IDENTIFIER(int i) { + return getToken(AlertExpressionParser.IDENTIFIER, i); + } + + public TerminalNode DOT() { + return getToken(AlertExpressionParser.DOT, 0); + } + + public TerminalNode NULL() { + return getToken(AlertExpressionParser.NULL, 0); + } + + public TerminalNode LPAREN() { + return getToken(AlertExpressionParser.LPAREN, 0); + } + + public SelectSqlContext selectSql() { + return getRuleContext(SelectSqlContext.class, 0); + } + + public TerminalNode RPAREN() { + return getToken(AlertExpressionParser.RPAREN, 0); + } + + public FunctionCallContext functionCall() { + return getRuleContext(FunctionCallContext.class, 0); + } + + public ConditionUnitContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_conditionUnit; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitConditionUnit(this); + else return visitor.visitChildren(this); + } + } + + public final ConditionUnitContext conditionUnit() throws RecognitionException { + ConditionUnitContext _localctx = new ConditionUnitContext(_ctx, getState()); + enterRule(_localctx, 42, RULE_conditionUnit); + try { + setState(311); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 35, _ctx)) { + case 1: + enterOuterAlt(_localctx, 1); + { + setState(299); + number(); + } + break; + case 2: + enterOuterAlt(_localctx, 2); + { + setState(300); + string(); + } + break; + case 3: + enterOuterAlt(_localctx, 3); + { + setState(301); + match(IDENTIFIER); + } + break; + case 4: + enterOuterAlt(_localctx, 4); + { + setState(302); + match(IDENTIFIER); + setState(303); + match(DOT); + setState(304); + match(IDENTIFIER); + } + break; + case 5: + enterOuterAlt(_localctx, 5); + { + setState(305); + match(NULL); + } + break; + case 6: + enterOuterAlt(_localctx, 6); + { + setState(306); + match(LPAREN); + setState(307); + selectSql(); + setState(308); + match(RPAREN); + } + break; + case 7: + enterOuterAlt(_localctx, 7); + { + setState(310); + functionCall(); + } + break; + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class PromqlContext extends ParserRuleContext { + public MetricSelectorContext metricSelector() { + return getRuleContext(MetricSelectorContext.class, 0); + } + + public InstantVectorOpContext instantVectorOp() { + return getRuleContext(InstantVectorOpContext.class, 0); + } + + public AggregationOperatorContext aggregationOperator() { + return getRuleContext(AggregationOperatorContext.class, 0); + } + + public TerminalNode LPAREN() { + return getToken(AlertExpressionParser.LPAREN, 0); + } + + public PromqlContext promql() { + return getRuleContext(PromqlContext.class, 0); + } + + public TerminalNode RPAREN() { + return getToken(AlertExpressionParser.RPAREN, 0); + } + + public TerminalNode BY() { + return getToken(AlertExpressionParser.BY, 0); + } + + public LabelListContext labelList() { + return getRuleContext(LabelListContext.class, 0); + } + + public FunctionCallContext functionCall() { + return getRuleContext(FunctionCallContext.class, 0); + } + + public TerminalNode IDENTIFIER() { + return getToken(AlertExpressionParser.IDENTIFIER, 0); + } + + public TerminalNode LBRACKET() { + return getToken(AlertExpressionParser.LBRACKET, 0); + } + + public List<DurationContext> duration() { + return getRuleContexts(DurationContext.class); + } + + public DurationContext duration(int i) { + return getRuleContext(DurationContext.class, i); + } + + public TerminalNode RBRACKET() { + return getToken(AlertExpressionParser.RBRACKET, 0); + } + + public TerminalNode COLON() { + return getToken(AlertExpressionParser.COLON, 0); + } + + public TerminalNode OFFSET() { + return getToken(AlertExpressionParser.OFFSET, 0); + } + + public PromqlContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_promql; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitPromql(this); + else return visitor.visitChildren(this); + } + } + + public final PromqlContext promql() throws RecognitionException { + return promql(0); + } + + private PromqlContext promql(int _p) throws RecognitionException { + ParserRuleContext _parentctx = _ctx; + int _parentState = getState(); + PromqlContext _localctx = new PromqlContext(_ctx, _parentState); + PromqlContext _prevctx = _localctx; + int _startState = 44; + enterRecursionRule(_localctx, 44, RULE_promql, _p); + int _la; + try { + int _alt; + enterOuterAlt(_localctx, 1); + { + setState(329); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 38, _ctx)) { + case 1: { + setState(314); + metricSelector(); + setState(316); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 36, _ctx)) { + case 1: { + setState(315); + instantVectorOp(); + } + break; + } + } + break; + case 2: { + setState(318); + aggregationOperator(); + setState(319); + match(LPAREN); + setState(320); + promql(0); + setState(323); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la == BY) { + { + setState(321); + match(BY); + setState(322); + labelList(); + } + } + + setState(325); + match(RPAREN); + } + break; + case 3: { + setState(327); + functionCall(); + } + break; + case 4: { + setState(328); + match(IDENTIFIER); + } + break; + } + _ctx.stop = _input.LT(-1); + setState(348); + _errHandler.sync(this); + _alt = getInterpreter().adaptivePredict(_input, 40, _ctx); + while (_alt != 2 && _alt != ATN.INVALID_ALT_NUMBER) { + if (_alt == 1) { + if (_parseListeners != null) triggerExitRuleEvent(); + _prevctx = _localctx; + { + setState(346); + _errHandler.sync(this); + switch (getInterpreter().adaptivePredict(_input, 39, _ctx)) { + case 1: { + _localctx = new PromqlContext(_parentctx, _parentState); + pushNewRecursionContext(_localctx, _startState, RULE_promql); + setState(331); + if (!(precpred(_ctx, 4))) + throw new FailedPredicateException(this, "precpred(_ctx, 4)"); + setState(332); + match(LBRACKET); + setState(333); + duration(); + setState(334); + match(RBRACKET); + } + break; + case 2: { + _localctx = new PromqlContext(_parentctx, _parentState); + pushNewRecursionContext(_localctx, _startState, RULE_promql); + setState(336); + if (!(precpred(_ctx, 3))) + throw new FailedPredicateException(this, "precpred(_ctx, 3)"); + setState(337); + match(LBRACKET); + setState(338); + duration(); + setState(339); + match(COLON); + setState(340); + duration(); + setState(341); + match(RBRACKET); + } + break; + case 3: { + _localctx = new PromqlContext(_parentctx, _parentState); + pushNewRecursionContext(_localctx, _startState, RULE_promql); + setState(343); + if (!(precpred(_ctx, 2))) + throw new FailedPredicateException(this, "precpred(_ctx, 2)"); + setState(344); + match(OFFSET); + setState(345); + duration(); + } + break; + } + } + } + setState(350); + _errHandler.sync(this); + _alt = getInterpreter().adaptivePredict(_input, 40, _ctx); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + unrollRecursionContexts(_parentctx); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class MetricSelectorContext extends ParserRuleContext { + public TerminalNode LBRACE() { + return getToken(AlertExpressionParser.LBRACE, 0); + } + + public TerminalNode RBRACE() { + return getToken(AlertExpressionParser.RBRACE, 0); + } + + public LabelMatcherListContext labelMatcherList() { + return getRuleContext(LabelMatcherListContext.class, 0); + } + + public MetricSelectorContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_metricSelector; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitMetricSelector(this); + else return visitor.visitChildren(this); + } + } + + public final MetricSelectorContext metricSelector() throws RecognitionException { + MetricSelectorContext _localctx = new MetricSelectorContext(_ctx, getState()); + enterRule(_localctx, 46, RULE_metricSelector); + int _la; + try { + enterOuterAlt(_localctx, 1); + { + setState(351); + match(LBRACE); + setState(353); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la == IDENTIFIER) { + { + setState(352); + labelMatcherList(); + } + } + + setState(355); + match(RBRACE); + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class LabelMatcherListContext extends ParserRuleContext { + public List<LabelMatcherItemContext> labelMatcherItem() { + return getRuleContexts(LabelMatcherItemContext.class); + } + + public LabelMatcherItemContext labelMatcherItem(int i) { + return getRuleContext(LabelMatcherItemContext.class, i); + } + + public List<TerminalNode> COMMA() { + return getTokens(AlertExpressionParser.COMMA); + } + + public TerminalNode COMMA(int i) { + return getToken(AlertExpressionParser.COMMA, i); + } + + public LabelMatcherListContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_labelMatcherList; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitLabelMatcherList(this); + else return visitor.visitChildren(this); + } + } + + public final LabelMatcherListContext labelMatcherList() throws RecognitionException { + LabelMatcherListContext _localctx = new LabelMatcherListContext(_ctx, getState()); + enterRule(_localctx, 48, RULE_labelMatcherList); + int _la; + try { + enterOuterAlt(_localctx, 1); + { + setState(357); + labelMatcherItem(); + setState(362); + _errHandler.sync(this); + _la = _input.LA(1); + while (_la == COMMA) { + { + { + setState(358); + match(COMMA); + setState(359); + labelMatcherItem(); + } + } + setState(364); + _errHandler.sync(this); + _la = _input.LA(1); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class LabelMatcherItemContext extends ParserRuleContext { + public TerminalNode IDENTIFIER() { + return getToken(AlertExpressionParser.IDENTIFIER, 0); + } + + public LabelMatcherOpContext labelMatcherOp() { + return getRuleContext(LabelMatcherOpContext.class, 0); + } + + public StringContext string() { + return getRuleContext(StringContext.class, 0); + } + + public LabelMatcherItemContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_labelMatcherItem; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitLabelMatcherItem(this); + else return visitor.visitChildren(this); + } + } + + public final LabelMatcherItemContext labelMatcherItem() throws RecognitionException { + LabelMatcherItemContext _localctx = new LabelMatcherItemContext(_ctx, getState()); + enterRule(_localctx, 50, RULE_labelMatcherItem); + try { + enterOuterAlt(_localctx, 1); + { + setState(365); + match(IDENTIFIER); + setState(366); + labelMatcherOp(); + setState(367); + string(); + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class LabelMatcherOpContext extends ParserRuleContext { + public TerminalNode EQ() { + return getToken(AlertExpressionParser.EQ, 0); + } + + public TerminalNode NE() { + return getToken(AlertExpressionParser.NE, 0); + } + + public LabelMatcherOpContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_labelMatcherOp; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitLabelMatcherOp(this); + else return visitor.visitChildren(this); + } + } + + public final LabelMatcherOpContext labelMatcherOp() throws RecognitionException { + LabelMatcherOpContext _localctx = new LabelMatcherOpContext(_ctx, getState()); + enterRule(_localctx, 52, RULE_labelMatcherOp); + int _la; + try { + enterOuterAlt(_localctx, 1); + { + setState(369); + _la = _input.LA(1); + if (!(_la == EQ || _la == NE)) { + _errHandler.recoverInline(this); + } else { + if (_input.LA(1) == Token.EOF) matchedEOF = true; + _errHandler.reportMatch(this); + consume(); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class LabelListContext extends ParserRuleContext { + public TerminalNode LPAREN() { + return getToken(AlertExpressionParser.LPAREN, 0); + } + + public List<TerminalNode> IDENTIFIER() { + return getTokens(AlertExpressionParser.IDENTIFIER); + } + + public TerminalNode IDENTIFIER(int i) { + return getToken(AlertExpressionParser.IDENTIFIER, i); + } + + public TerminalNode RPAREN() { + return getToken(AlertExpressionParser.RPAREN, 0); + } + + public List<TerminalNode> COMMA() { + return getTokens(AlertExpressionParser.COMMA); + } + + public TerminalNode COMMA(int i) { + return getToken(AlertExpressionParser.COMMA, i); + } + + public LabelListContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_labelList; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitLabelList(this); + else return visitor.visitChildren(this); + } + } + + public final LabelListContext labelList() throws RecognitionException { + LabelListContext _localctx = new LabelListContext(_ctx, getState()); + enterRule(_localctx, 54, RULE_labelList); + int _la; + try { + enterOuterAlt(_localctx, 1); + { + setState(371); + match(LPAREN); + setState(372); + match(IDENTIFIER); + setState(377); + _errHandler.sync(this); + _la = _input.LA(1); + while (_la == COMMA) { + { + { + setState(373); + match(COMMA); + setState(374); + match(IDENTIFIER); + } + } + setState(379); + _errHandler.sync(this); + _la = _input.LA(1); + } + setState(380); + match(RPAREN); + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class InstantVectorOpContext extends ParserRuleContext { + public TerminalNode LBRACKET() { + return getToken(AlertExpressionParser.LBRACKET, 0); + } + + public DurationContext duration() { + return getRuleContext(DurationContext.class, 0); + } + + public TerminalNode RBRACKET() { + return getToken(AlertExpressionParser.RBRACKET, 0); + } + + public InstantVectorOpContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_instantVectorOp; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitInstantVectorOp(this); + else return visitor.visitChildren(this); + } + } + + public final InstantVectorOpContext instantVectorOp() throws RecognitionException { + InstantVectorOpContext _localctx = new InstantVectorOpContext(_ctx, getState()); + enterRule(_localctx, 56, RULE_instantVectorOp); + try { + enterOuterAlt(_localctx, 1); + { + setState(382); + match(LBRACKET); + setState(383); + duration(); + setState(384); + match(RBRACKET); + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + @SuppressWarnings("CheckReturnValue") + public static class AggregationOperatorContext extends ParserRuleContext { + public TerminalNode SUM() { + return getToken(AlertExpressionParser.SUM, 0); + } + + public TerminalNode AVG() { + return getToken(AlertExpressionParser.AVG, 0); + } + + public TerminalNode COUNT() { + return getToken(AlertExpressionParser.COUNT, 0); + } + + public TerminalNode MIN() { + return getToken(AlertExpressionParser.MIN, 0); + } + + public TerminalNode MAX() { + return getToken(AlertExpressionParser.MAX, 0); + } + + public TerminalNode STDDEV() { + return getToken(AlertExpressionParser.STDDEV, 0); + } + + public TerminalNode STDVAR() { + return getToken(AlertExpressionParser.STDVAR, 0); + } + + public TerminalNode TOPK() { + return getToken(AlertExpressionParser.TOPK, 0); + } + + public TerminalNode BOTTOMK() { + return getToken(AlertExpressionParser.BOTTOMK, 0); + } + + public TerminalNode QUANTILE() { + return getToken(AlertExpressionParser.QUANTILE, 0); + } + + public AggregationOperatorContext(ParserRuleContext parent, int invokingState) { + super(parent, invokingState); + } + + @Override + public int getRuleIndex() { + return RULE_aggregationOperator; + } + + @Override + public <T> T accept(ParseTreeVisitor<? extends T> visitor) { + if (visitor instanceof AlertExpressionVisitor) + return ((AlertExpressionVisitor<? extends T>) visitor).visitAggregationOperator(this); + else return visitor.visitChildren(this); + } + } + + public final AggregationOperatorContext aggregationOperator() throws RecognitionException { + AggregationOperatorContext _localctx = new AggregationOperatorContext(_ctx, getState()); + enterRule(_localctx, 58, RULE_aggregationOperator); + int _la; + try { + enterOuterAlt(_localctx, 1); + { + setState(386); + _la = _input.LA(1); + if (!((((_la) & ~0x3f) == 0 && ((1L << _la) & 121324437504L) != 0))) { + _errHandler.recoverInline(this); + } else { + if (_input.LA(1) == Token.EOF) matchedEOF = true; + _errHandler.reportMatch(this); + consume(); + } + } + } catch (RecognitionException re) { + _localctx.exception = re; + _errHandler.reportError(this, re); + _errHandler.recover(this, re); + } finally { + exitRule(); + } + return _localctx; + } + + public boolean sempred(RuleContext _localctx, int ruleIndex, int predIndex) { + switch (ruleIndex) { + case 1: + return expr_sempred((ExprContext) _localctx, predIndex); + case 18: + return conditionList_sempred((ConditionListContext) _localctx, predIndex); + case 22: + return promql_sempred((PromqlContext) _localctx, predIndex); + } + return true; + } + + private boolean expr_sempred(ExprContext _localctx, int predIndex) { + switch (predIndex) { + case 0: + return precpred(_ctx, 9); + case 1: + return precpred(_ctx, 8); + case 2: + return precpred(_ctx, 7); + case 3: + return precpred(_ctx, 6); + } + return true; + } + + private boolean conditionList_sempred(ConditionListContext _localctx, int predIndex) { + switch (predIndex) { + case 4: + return precpred(_ctx, 4); + case 5: + return precpred(_ctx, 3); + } + return true; + } + + private boolean promql_sempred(PromqlContext _localctx, int predIndex) { + switch (predIndex) { + case 6: + return precpred(_ctx, 4); + case 7: + return precpred(_ctx, 3); + case 8: + return precpred(_ctx, 2); + } + return true; + } + + public static final String _serializedATN = "\u0004\u0001F\u0185\u0002\u0000\u0007\u0000\u0002\u0001\u0007\u0001\u0002" + "\u0002\u0007\u0002\u0002\u0003\u0007\u0003\u0002\u0004\u0007\u0004\u0002" + "\u0005\u0007\u0005\u0002\u0006\u0007\u0006\u0002\u0007\u0007\u0007\u0002" + "\b\u0007\b\u0002\t\u0007\t\u0002\n\u0007\n\u0002\u000b\u0007\u000b\u0002" + "\f\u0007\f\u0002\r\u0007\r\u0002\u000e\u0007\u000e\u0002\u000f\u0007\u000f" + "\u0002\u0010\u0007\u0010\u0002\u0011\u0007\u0011\u0002\u0012\u0007\u0012" + "\u0002\u0013\u0007\u0013\u0002\u0014\u0007\u0014\u0002\u0015\u0007\u0015" + "\u0002\u0016\u0007\u0016\u0002\u0017\u0007\u0017\u0002\u0018\u0007\u0018" + "\u0002\u0019\u0007\u0019\u0002\u001a\u0007\u001a\u0002\u001b\u0007\u001b" + "\u0002\u001c\u0007\u001c\u0002\u001d\u0007\u001d\u0001\u0000\u0001\u0000" + "\u0001\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" + "\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" + "\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" + "\u0001\u0001\u0003\u0001R\b\u0001\u0001\u0001\u0001\u0001\u0001\u0001" + "\u0003\u0001W\b\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" + "\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" + "\u0005\u0001c\b\u0001\n\u0001\f\u0001f\t\u0001\u0001\u0002\u0001\u0002" + "\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0003\u0001\u0003\u0001\u0003" + "\u0005\u0003p\b\u0003\n\u0003\f\u0003s\t\u0003\u0001\u0004\u0001\u0004" + "\u0001\u0004\u0001\u0004\u0003\u0004y\b\u0004\u0001\u0005\u0001\u0005" + "\u0001\u0006\u0001\u0006\u0001\u0007\u0001\u0007\u0001\b\u0001\b\u0001" + "\t\u0001\t\u0001\t\u0001\t\u0001\t\u0001\t\u0003\t\u0089\b\t\u0001\t\u0001" + "\t\u0001\t\u0003\t\u008e\b\t\u0001\t\u0001\t\u0003\t\u0092\b\t\u0001\t" + "\u0001\t\u0001\t\u0003\t\u0097\b\t\u0001\t\u0001\t\u0003\t\u009b\b\t\u0001" + "\n\u0001\n\u0001\n\u0005\n\u00a0\b\n\n\n\f\n\u00a3\t\n\u0001\u000b\u0001" + "\u000b\u0003\u000b\u00a7\b\u000b\u0001\u000b\u0003\u000b\u00aa\b\u000b" + "\u0001\u000b\u0001\u000b\u0003\u000b\u00ae\b\u000b\u0001\u000b\u0003\u000b" + "\u00b1\b\u000b\u0001\u000b\u0001\u000b\u0003\u000b\u00b5\b\u000b\u0001" + "\u000b\u0003\u000b\u00b8\b\u000b\u0001\u000b\u0001\u000b\u0001\u000b\u0001" + "\u000b\u0003\u000b\u00be\b\u000b\u0001\u000b\u0003\u000b\u00c1\b\u000b" + "\u0003\u000b\u00c3\b\u000b\u0001\f\u0001\f\u0001\f\u0005\f\u00c8\b\f\n" + "\f\f\f\u00cb\t\f\u0001\r\u0001\r\u0001\r\u0005\r\u00d0\b\r\n\r\f\r\u00d3" + "\t\r\u0001\u000e\u0001\u000e\u0003\u000e\u00d7\b\u000e\u0001\u000e\u0001" + "\u000e\u0003\u000e\u00db\b\u000e\u0003\u000e\u00dd\b\u000e\u0001\u000f" + "\u0001\u000f\u0001\u0010\u0001\u0010\u0001\u0010\u0005\u0010\u00e4\b\u0010" + "\n\u0010\f\u0010\u00e7\t\u0010\u0001\u0011\u0001\u0011\u0003\u0011\u00eb" + "\b\u0011\u0001\u0011\u0003\u0011\u00ee\b\u0011\u0001\u0011\u0001\u0011" + "\u0001\u0011\u0001\u0011\u0001\u0011\u0001\u0011\u0003\u0011\u00f6\b\u0011" + "\u0001\u0012\u0001\u0012\u0001\u0012\u0001\u0012\u0001\u0012\u0001\u0012" + "\u0003\u0012\u00fe\b\u0012\u0001\u0012\u0001\u0012\u0001\u0012\u0001\u0012" + "\u0001\u0012\u0001\u0012\u0005\u0012\u0106\b\u0012\n\u0012\f\u0012\u0109" + "\t\u0012\u0001\u0013\u0001\u0013\u0001\u0013\u0001\u0013\u0001\u0013\u0001" + "\u0013\u0001\u0013\u0001\u0013\u0001\u0013\u0001\u0013\u0001\u0013\u0001" + "\u0013\u0001\u0013\u0001\u0013\u0001\u0013\u0003\u0013\u011a\b\u0013\u0001" + "\u0014\u0001\u0014\u0001\u0014\u0001\u0014\u0001\u0014\u0001\u0014\u0001" + "\u0014\u0001\u0014\u0001\u0014\u0001\u0014\u0001\u0014\u0001\u0014\u0001" + "\u0014\u0001\u0014\u0003\u0014\u012a\b\u0014\u0001\u0015\u0001\u0015\u0001" + "\u0015\u0001\u0015\u0001\u0015\u0001\u0015\u0001\u0015\u0001\u0015\u0001" + "\u0015\u0001\u0015\u0001\u0015\u0001\u0015\u0003\u0015\u0138\b\u0015\u0001" + "\u0016\u0001\u0016\u0001\u0016\u0003\u0016\u013d\b\u0016\u0001\u0016\u0001" + "\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0003\u0016\u0144\b\u0016\u0001" + "\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0003\u0016\u014a\b\u0016\u0001" + "\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0001" + "\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0001" + "\u0016\u0001\u0016\u0001\u0016\u0005\u0016\u015b\b\u0016\n\u0016\f\u0016" + "\u015e\t\u0016\u0001\u0017\u0001\u0017\u0003\u0017\u0162\b\u0017\u0001" + "\u0017\u0001\u0017\u0001\u0018\u0001\u0018\u0001\u0018\u0005\u0018\u0169" + "\b\u0018\n\u0018\f\u0018\u016c\t\u0018\u0001\u0019\u0001\u0019\u0001\u0019" + "\u0001\u0019\u0001\u001a\u0001\u001a\u0001\u001b\u0001\u001b\u0001\u001b" + "\u0001\u001b\u0005\u001b\u0178\b\u001b\n\u001b\f\u001b\u017b\t\u001b\u0001" + "\u001b\u0001\u001b\u0001\u001c\u0001\u001c\u0001\u001c\u0001\u001c\u0001" + "\u001d\u0001\u001d\u0001\u001d\u0000\u0003\u0002$,\u001e\u0000\u0002\u0004" + "\u0006\b\n\f\u000e\u0010\u0012\u0014\u0016\u0018\u001a\u001c\u001e \"" + "$&(*,.02468:\u0000\u0006\u0001\u0000-2\u0001\u0000>@\u0004\u0000\u0017" + "\u001b\u001f!%*CC\u0001\u0000\u000f\u0010\u0001\u000012\u0002\u0000\u0017" + "\u001d\"$\u01af\u0000<\u0001\u0000\u0000\u0000\u0002Q\u0001\u0000\u0000" + "\u0000\u0004g\u0001\u0000\u0000\u0000\u0006l\u0001\u0000\u0000\u0000\b" + "x\u0001\u0000\u0000\u0000\nz\u0001\u0000\u0000\u0000\f|\u0001\u0000\u0000" + "\u0000\u000e~\u0001\u0000\u0000\u0000\u0010\u0080\u0001\u0000\u0000\u0000" + "\u0012\u0082\u0001\u0000\u0000\u0000\u0014\u009c\u0001\u0000\u0000\u0000" + "\u0016\u00c2\u0001\u0000\u0000\u0000\u0018\u00c4\u0001\u0000\u0000\u0000" + "\u001a\u00cc\u0001\u0000\u0000\u0000\u001c\u00dc\u0001\u0000\u0000\u0000" + "\u001e\u00de\u0001\u0000\u0000\u0000 \u00e0\u0001\u0000\u0000\u0000\"" + "\u00f5\u0001\u0000\u0000\u0000$\u00fd\u0001\u0000\u0000\u0000&\u0119\u0001" + "\u0000\u0000\u0000(\u0129\u0001\u0000\u0000\u0000*\u0137\u0001\u0000\u0000" + "\u0000,\u0149\u0001\u0000\u0000\u0000.\u015f\u0001\u0000\u0000\u00000" + "\u0165\u0001\u0000\u0000\u00002\u016d\u0001\u0000\u0000\u00004\u0171\u0001" + "\u0000\u0000\u00006\u0173\u0001\u0000\u0000\u00008\u017e\u0001\u0000\u0000" + "\u0000:\u0182\u0001\u0000\u0000\u0000<=\u0003\u0002\u0001\u0000=>\u0005" + "\u0000\u0000\u0001>\u0001\u0001\u0000\u0000\u0000?@\u0006\u0001\uffff" + "\uffff\u0000@A\u00054\u0000\u0000AB\u0003\u0002\u0001\u0000BC\u00055\u0000" + "\u0000CR\u0001\u0000\u0000\u0000DR\u0003,\u0016\u0000ER\u0003\u0012\t" + "\u0000FR\u0003\n\u0005\u0000GH\u0005+\u0000\u0000HI\u00054\u0000\u0000" + "IJ\u0003\f\u0006\u0000JK\u00055\u0000\u0000KR\u0001\u0000\u0000\u0000" + "LM\u0005,\u0000\u0000MN\u00054\u0000\u0000NO\u0003\f\u0006\u0000OP\u0005" + "5\u0000\u0000PR\u0001\u0000\u0000\u0000Q?\u0001\u0000\u0000\u0000QD\u0001" + "\u0000\u0000\u0000QE\u0001\u0000\u0000\u0000QF\u0001\u0000\u0000\u0000" + "QG\u0001\u0000\u0000\u0000QL\u0001\u0000\u0000\u0000Rd\u0001\u0000\u0000" + "\u0000ST\n\t\u0000\u0000TV\u0007\u0000\u0000\u0000UW\u00053\u0000\u0000" + "VU\u0001\u0000\u0000\u0000VW\u0001\u0000\u0000\u0000WX\u0001\u0000\u0000" + "\u0000Xc\u0003\u0002\u0001\nYZ\n\b\u0000\u0000Z[\u0005\u0001\u0000\u0000" + "[c\u0003\u0002\u0001\t\\]\n\u0007\u0000\u0000]^\u0005\u0003\u0000\u0000" + "^c\u0003\u0002\u0001\b_`\n\u0006\u0000\u0000`a\u0005\u0002\u0000\u0000" + "ac\u0003\u0002\u0001\u0007bS\u0001\u0000\u0000\u0000bY\u0001\u0000\u0000" + "\u0000b\\\u0001\u0000\u0000\u0000b_\u0001\u0000\u0000\u0000cf\u0001\u0000" + "\u0000\u0000db\u0001\u0000\u0000\u0000de\u0001\u0000\u0000\u0000e\u0003" + "\u0001\u0000\u0000\u0000fd\u0001\u0000\u0000\u0000gh\u0003\u0010\b\u0000" + "hi\u00054\u0000\u0000ij\u0003\u0006\u0003\u0000jk\u00055\u0000\u0000k" + "\u0005\u0001\u0000\u0000\u0000lq\u0003\b\u0004\u0000mn\u0005:\u0000\u0000" + "np\u0003\b\u0004\u0000om\u0001\u0000\u0000\u0000ps\u0001\u0000\u0000\u0000" + "qo\u0001\u0000\u0000\u0000qr\u0001\u0000\u0000\u0000r\u0007\u0001\u0000" + "\u0000\u0000sq\u0001\u0000\u0000\u0000ty\u0003\u0002\u0001\u0000uy\u0005" + "\u0016\u0000\u0000vy\u0003\f\u0006\u0000wy\u0003\u000e\u0007\u0000xt\u0001" + "\u0000\u0000\u0000xu\u0001\u0000\u0000\u0000xv\u0001\u0000\u0000\u0000" + "xw\u0001\u0000\u0000\u0000y\t\u0001\u0000\u0000\u0000z{\u0007\u0001\u0000" + "\u0000{\u000b\u0001\u0000\u0000\u0000|}\u0005B\u0000\u0000}\r\u0001\u0000" + "\u0000\u0000~\u007f\u0005A\u0000\u0000\u007f\u000f\u0001\u0000\u0000\u0000" + "\u0080\u0081\u0007\u0002\u0000\u0000\u0081\u0011\u0001\u0000\u0000\u0000" + "\u0082\u0083\u0005\u0005\u0000\u0000\u0083\u0084\u0003\u0014\n\u0000\u0084" + "\u0085\u0005\u0006\u0000\u0000\u0085\u0088\u0003 \u0010\u0000\u0086\u0087" + "\u0005\u0007\u0000\u0000\u0087\u0089\u0003$\u0012\u0000\u0088\u0086\u0001" + "\u0000\u0000\u0000\u0088\u0089\u0001\u0000\u0000\u0000\u0089\u008d\u0001" + "\u0000\u0000\u0000\u008a\u008b\u0005\b\u0000\u0000\u008b\u008c\u0005\t" + "\u0000\u0000\u008c\u008e\u0003\u0018\f\u0000\u008d\u008a\u0001\u0000\u0000" + "\u0000\u008d\u008e\u0001\u0000\u0000\u0000\u008e\u0091\u0001\u0000\u0000" + "\u0000\u008f\u0090\u0005\n\u0000\u0000\u0090\u0092\u0003$\u0012\u0000" + "\u0091\u008f\u0001\u0000\u0000\u0000\u0091\u0092\u0001\u0000\u0000\u0000" + "\u0092\u0096\u0001\u0000\u0000\u0000\u0093\u0094\u0005\u000b\u0000\u0000" + "\u0094\u0095\u0005\t\u0000\u0000\u0095\u0097\u0003\u001a\r\u0000\u0096" + "\u0093\u0001\u0000\u0000\u0000\u0096\u0097\u0001\u0000\u0000\u0000\u0097" + "\u009a\u0001\u0000\u0000\u0000\u0098\u0099\u0005\f\u0000\u0000\u0099\u009b" + "\u0003\u001e\u000f\u0000\u009a\u0098\u0001\u0000\u0000\u0000\u009a\u009b" + "\u0001\u0000\u0000\u0000\u009b\u0013\u0001\u0000\u0000\u0000\u009c\u00a1" + "\u0003\u0016\u000b\u0000\u009d\u009e\u0005:\u0000\u0000\u009e\u00a0\u0003" + "\u0016\u000b\u0000\u009f\u009d\u0001\u0000\u0000\u0000\u00a0\u00a3\u0001" + "\u0000\u0000\u0000\u00a1\u009f\u0001\u0000\u0000\u0000\u00a1\u00a2\u0001" + "\u0000\u0000\u0000\u00a2\u0015\u0001\u0000\u0000\u0000\u00a3\u00a1\u0001" + "\u0000\u0000\u0000\u00a4\u00a9\u0003\u0004\u0002\u0000\u00a5\u00a7\u0005" + "\u000e\u0000\u0000\u00a6\u00a5\u0001\u0000\u0000\u0000\u00a6\u00a7\u0001" + "\u0000\u0000\u0000\u00a7\u00a8\u0001\u0000\u0000\u0000\u00a8\u00aa\u0005" + "C\u0000\u0000\u00a9\u00a6\u0001\u0000\u0000\u0000\u00a9\u00aa\u0001\u0000" + "\u0000\u0000\u00aa\u00c3\u0001\u0000\u0000\u0000\u00ab\u00b0\u0005C\u0000" + "\u0000\u00ac\u00ae\u0005\u000e\u0000\u0000\u00ad\u00ac\u0001\u0000\u0000" + "\u0000\u00ad\u00ae\u0001\u0000\u0000\u0000\u00ae\u00af\u0001\u0000\u0000" + "\u0000\u00af\u00b1\u0005C\u0000\u0000\u00b0\u00ad\u0001\u0000\u0000\u0000" + "\u00b0\u00b1\u0001\u0000\u0000\u0000\u00b1\u00c3\u0001\u0000\u0000\u0000" + "\u00b2\u00b7\u0005\u0016\u0000\u0000\u00b3\u00b5\u0005\u000e\u0000\u0000" + "\u00b4\u00b3\u0001\u0000\u0000\u0000\u00b4\u00b5\u0001\u0000\u0000\u0000" + "\u00b5\u00b6\u0001\u0000\u0000\u0000\u00b6\u00b8\u0005C\u0000\u0000\u00b7" + "\u00b4\u0001\u0000\u0000\u0000\u00b7\u00b8\u0001\u0000\u0000\u0000\u00b8" + "\u00c3\u0001\u0000\u0000\u0000\u00b9\u00ba\u0005C\u0000\u0000\u00ba\u00bb" + "\u0005;\u0000\u0000\u00bb\u00c0\u0005C\u0000\u0000\u00bc\u00be\u0005\u000e" + "\u0000\u0000\u00bd\u00bc\u0001\u0000\u0000\u0000\u00bd\u00be\u0001\u0000" + "\u0000\u0000\u00be\u00bf\u0001\u0000\u0000\u0000\u00bf\u00c1\u0005C\u0000" + "\u0000\u00c0\u00bd\u0001\u0000\u0000\u0000\u00c0\u00c1\u0001\u0000\u0000" + "\u0000\u00c1\u00c3\u0001\u0000\u0000\u0000\u00c2\u00a4\u0001\u0000\u0000" + "\u0000\u00c2\u00ab\u0001\u0000\u0000\u0000\u00c2\u00b2\u0001\u0000\u0000" + "\u0000\u00c2\u00b9\u0001\u0000\u0000\u0000\u00c3\u0017\u0001\u0000\u0000" + "\u0000\u00c4\u00c9\u0005C\u0000\u0000\u00c5\u00c6\u0005:\u0000\u0000\u00c6" + "\u00c8\u0005C\u0000\u0000\u00c7\u00c5\u0001\u0000\u0000\u0000\u00c8\u00cb" + "\u0001\u0000\u0000\u0000\u00c9\u00c7\u0001\u0000\u0000\u0000\u00c9\u00ca" + "\u0001\u0000\u0000\u0000\u00ca\u0019\u0001\u0000\u0000\u0000\u00cb\u00c9" + "\u0001\u0000\u0000\u0000\u00cc\u00d1\u0003\u001c\u000e\u0000\u00cd\u00ce" + "\u0005:\u0000\u0000\u00ce\u00d0\u0003\u001c\u000e\u0000\u00cf\u00cd\u0001" + "\u0000\u0000\u0000\u00d0\u00d3\u0001\u0000\u0000\u0000\u00d1\u00cf\u0001" + "\u0000\u0000\u0000\u00d1\u00d2\u0001\u0000\u0000\u0000\u00d2\u001b\u0001" + "\u0000\u0000\u0000\u00d3\u00d1\u0001\u0000\u0000\u0000\u00d4\u00d6\u0005" + "C\u0000\u0000\u00d5\u00d7\u0007\u0003\u0000\u0000\u00d6\u00d5\u0001\u0000" + "\u0000\u0000\u00d6\u00d7\u0001\u0000\u0000\u0000\u00d7\u00dd\u0001\u0000" + "\u0000\u0000\u00d8\u00da\u0003\u0004\u0002\u0000\u00d9\u00db\u0007\u0003" + "\u0000\u0000\u00da\u00d9\u0001\u0000\u0000\u0000\u00da\u00db\u0001\u0000" + "\u0000\u0000\u00db\u00dd\u0001\u0000\u0000\u0000\u00dc\u00d4\u0001\u0000" + "\u0000\u0000\u00dc\u00d8\u0001\u0000\u0000\u0000\u00dd\u001d\u0001\u0000" + "\u0000\u0000\u00de\u00df\u0005@\u0000\u0000\u00df\u001f\u0001\u0000\u0000" + "\u0000\u00e0\u00e5\u0003\"\u0011\u0000\u00e1\u00e2\u0005:\u0000\u0000" + "\u00e2\u00e4\u0003\"\u0011\u0000\u00e3\u00e1\u0001\u0000\u0000\u0000\u00e4" + "\u00e7\u0001\u0000\u0000\u0000\u00e5\u00e3\u0001\u0000\u0000\u0000\u00e5" + "\u00e6\u0001\u0000\u0000\u0000\u00e6!\u0001\u0000\u0000\u0000\u00e7\u00e5" + "\u0001\u0000\u0000\u0000\u00e8\u00ed\u0005C\u0000\u0000\u00e9\u00eb\u0005" + "\u000e\u0000\u0000\u00ea\u00e9\u0001\u0000\u0000\u0000\u00ea\u00eb\u0001" + "\u0000\u0000\u0000\u00eb\u00ec\u0001\u0000\u0000\u0000\u00ec\u00ee\u0005" + "C\u0000\u0000\u00ed\u00ea\u0001\u0000\u0000\u0000\u00ed\u00ee\u0001\u0000" + "\u0000\u0000\u00ee\u00f6\u0001\u0000\u0000\u0000\u00ef\u00f0\u00054\u0000" + "\u0000\u00f0\u00f1\u0003\u0012\t\u0000\u00f1\u00f2\u00055\u0000\u0000" + "\u00f2\u00f3\u0005\u000e\u0000\u0000\u00f3\u00f4\u0005C\u0000\u0000\u00f4" + "\u00f6\u0001\u0000\u0000\u0000\u00f5\u00e8\u0001\u0000\u0000\u0000\u00f5" + "\u00ef\u0001\u0000\u0000\u0000\u00f6#\u0001\u0000\u0000\u0000\u00f7\u00f8" + "\u0006\u0012\uffff\uffff\u0000\u00f8\u00fe\u0003(\u0014\u0000\u00f9\u00fa" + "\u00054\u0000\u0000\u00fa\u00fb\u0003$\u0012\u0000\u00fb\u00fc\u00055" + "\u0000\u0000\u00fc\u00fe\u0001\u0000\u0000\u0000\u00fd\u00f7\u0001\u0000" + "\u0000\u0000\u00fd\u00f9\u0001\u0000\u0000\u0000\u00fe\u0107\u0001\u0000" + "\u0000\u0000\u00ff\u0100\n\u0004\u0000\u0000\u0100\u0101\u0005\u0001\u0000" + "\u0000\u0101\u0106\u0003$\u0012\u0005\u0102\u0103\n\u0003\u0000\u0000" + "\u0103\u0104\u0005\u0002\u0000\u0000\u0104\u0106\u0003$\u0012\u0004\u0105" + "\u00ff\u0001\u0000\u0000\u0000\u0105\u0102\u0001\u0000\u0000\u0000\u0106" + "\u0109\u0001\u0000\u0000\u0000\u0107\u0105\u0001\u0000\u0000\u0000\u0107" + "\u0108\u0001\u0000\u0000\u0000\u0108%\u0001\u0000\u0000\u0000\u0109\u0107" + "\u0001\u0000\u0000\u0000\u010a\u011a\u00051\u0000\u0000\u010b\u011a\u0005" + "/\u0000\u0000\u010c\u011a\u0005-\u0000\u0000\u010d\u011a\u00050\u0000" + "\u0000\u010e\u011a\u0005.\u0000\u0000\u010f\u011a\u00052\u0000\u0000\u0110" + "\u011a\u0005\u0014\u0000\u0000\u0111\u0112\u0005\u0004\u0000\u0000\u0112" + "\u011a\u0005\u0014\u0000\u0000\u0113\u011a\u0005\u0011\u0000\u0000\u0114" + "\u0115\u0005\u0004\u0000\u0000\u0115\u011a\u0005\u0011\u0000\u0000\u0116" + "\u011a\u0005\u0012\u0000\u0000\u0117\u0118\u0005\u0012\u0000\u0000\u0118" + "\u011a\u0005\u0004\u0000\u0000\u0119\u010a\u0001\u0000\u0000\u0000\u0119" + "\u010b\u0001\u0000\u0000\u0000\u0119\u010c\u0001\u0000\u0000\u0000\u0119" + "\u010d\u0001\u0000\u0000\u0000\u0119\u010e\u0001\u0000\u0000\u0000\u0119" + "\u010f\u0001\u0000\u0000\u0000\u0119\u0110\u0001\u0000\u0000\u0000\u0119" + "\u0111\u0001\u0000\u0000\u0000\u0119\u0113\u0001\u0000\u0000\u0000\u0119" + "\u0114\u0001\u0000\u0000\u0000\u0119\u0116\u0001\u0000\u0000\u0000\u0119" + "\u0117\u0001\u0000\u0000\u0000\u011a\'\u0001\u0000\u0000\u0000\u011b\u011c" + "\u0003*\u0015\u0000\u011c\u011d\u0003&\u0013\u0000\u011d\u011e\u0003*" + "\u0015\u0000\u011e\u012a\u0001\u0000\u0000\u0000\u011f\u0120\u00054\u0000" + "\u0000\u0120\u0121\u0003(\u0014\u0000\u0121\u0122\u00055\u0000\u0000\u0122" + "\u012a\u0001\u0000\u0000\u0000\u0123\u0124\u0005C\u0000\u0000\u0124\u0125" + "\u0005\u0015\u0000\u0000\u0125\u0126\u0003\n\u0005\u0000\u0126\u0127\u0005" + "\u0001\u0000\u0000\u0127\u0128\u0003\n\u0005\u0000\u0128\u012a\u0001\u0000" + "\u0000\u0000\u0129\u011b\u0001\u0000\u0000\u0000\u0129\u011f\u0001\u0000" + "\u0000\u0000\u0129\u0123\u0001\u0000\u0000\u0000\u012a)\u0001\u0000\u0000" + "\u0000\u012b\u0138\u0003\n\u0005\u0000\u012c\u0138\u0003\f\u0006\u0000" + "\u012d\u0138\u0005C\u0000\u0000\u012e\u012f\u0005C\u0000\u0000\u012f\u0130" + "\u0005;\u0000\u0000\u0130\u0138\u0005C\u0000\u0000\u0131\u0138\u0005\u0013" + "\u0000\u0000\u0132\u0133\u00054\u0000\u0000\u0133\u0134\u0003\u0012\t" + "\u0000\u0134\u0135\u00055\u0000\u0000\u0135\u0138\u0001\u0000\u0000\u0000" + "\u0136\u0138\u0003\u0004\u0002\u0000\u0137\u012b\u0001\u0000\u0000\u0000" + "\u0137\u012c\u0001\u0000\u0000\u0000\u0137\u012d\u0001\u0000\u0000\u0000" + "\u0137\u012e\u0001\u0000\u0000\u0000\u0137\u0131\u0001\u0000\u0000\u0000" + "\u0137\u0132\u0001\u0000\u0000\u0000\u0137\u0136\u0001\u0000\u0000\u0000" + "\u0138+\u0001\u0000\u0000\u0000\u0139\u013a\u0006\u0016\uffff\uffff\u0000" + "\u013a\u013c\u0003.\u0017\u0000\u013b\u013d\u00038\u001c\u0000\u013c\u013b" + "\u0001\u0000\u0000\u0000\u013c\u013d\u0001\u0000\u0000\u0000\u013d\u014a" + "\u0001\u0000\u0000\u0000\u013e\u013f\u0003:\u001d\u0000\u013f\u0140\u0005" + "4\u0000\u0000\u0140\u0143\u0003,\u0016\u0000\u0141\u0142\u0005\t\u0000" + "\u0000\u0142\u0144\u00036\u001b\u0000\u0143\u0141\u0001\u0000\u0000\u0000" + "\u0143\u0144\u0001\u0000\u0000\u0000\u0144\u0145\u0001\u0000\u0000\u0000" + "\u0145\u0146\u00055\u0000\u0000\u0146\u014a\u0001\u0000\u0000\u0000\u0147" + "\u014a\u0003\u0004\u0002\u0000\u0148\u014a\u0005C\u0000\u0000\u0149\u0139" + "\u0001\u0000\u0000\u0000\u0149\u013e\u0001\u0000\u0000\u0000\u0149\u0147" + "\u0001\u0000\u0000\u0000\u0149\u0148\u0001\u0000\u0000\u0000\u014a\u015c" + "\u0001\u0000\u0000\u0000\u014b\u014c\n\u0004\u0000\u0000\u014c\u014d\u0005" + "8\u0000\u0000\u014d\u014e\u0003\u000e\u0007\u0000\u014e\u014f\u00059\u0000" + "\u0000\u014f\u015b\u0001\u0000\u0000\u0000\u0150\u0151\n\u0003\u0000\u0000" + "\u0151\u0152\u00058\u0000\u0000\u0152\u0153\u0003\u000e\u0007\u0000\u0153" + "\u0154\u0005<\u0000\u0000\u0154\u0155\u0003\u000e\u0007\u0000\u0155\u0156" + "\u00059\u0000\u0000\u0156\u015b\u0001\u0000\u0000\u0000\u0157\u0158\n" + "\u0002\u0000\u0000\u0158\u0159\u0005\r\u0000\u0000\u0159\u015b\u0003\u000e" + "\u0007\u0000\u015a\u014b\u0001\u0000\u0000\u0000\u015a\u0150\u0001\u0000" + "\u0000\u0000\u015a\u0157\u0001\u0000\u0000\u0000\u015b\u015e\u0001\u0000" + "\u0000\u0000\u015c\u015a\u0001\u0000\u0000\u0000\u015c\u015d\u0001\u0000" + "\u0000\u0000\u015d-\u0001\u0000\u0000\u0000\u015e\u015c\u0001\u0000\u0000" + "\u0000\u015f\u0161\u00056\u0000\u0000\u0160\u0162\u00030\u0018\u0000\u0161" + "\u0160\u0001\u0000\u0000\u0000\u0161\u0162\u0001\u0000\u0000\u0000\u0162" + "\u0163\u0001\u0000\u0000\u0000\u0163\u0164\u00057\u0000\u0000\u0164/\u0001" + "\u0000\u0000\u0000\u0165\u016a\u00032\u0019\u0000\u0166\u0167\u0005:\u0000" + "\u0000\u0167\u0169\u00032\u0019\u0000\u0168\u0166\u0001\u0000\u0000\u0000" + "\u0169\u016c\u0001\u0000\u0000\u0000\u016a\u0168\u0001\u0000\u0000\u0000" + "\u016a\u016b\u0001\u0000\u0000\u0000\u016b1\u0001\u0000\u0000\u0000\u016c" + "\u016a\u0001\u0000\u0000\u0000\u016d\u016e\u0005C\u0000\u0000\u016e\u016f" + "\u00034\u001a\u0000\u016f\u0170\u0003\f\u0006\u0000\u01703\u0001\u0000" + "\u0000\u0000\u0171\u0172\u0007\u0004\u0000\u0000\u01725\u0001\u0000\u0000" + "\u0000\u0173\u0174\u00054\u0000\u0000\u0174\u0179\u0005C\u0000\u0000\u0175" + "\u0176\u0005:\u0000\u0000\u0176\u0178\u0005C\u0000\u0000\u0177\u0175\u0001" + "\u0000\u0000\u0000\u0178\u017b\u0001\u0000\u0000\u0000\u0179\u0177\u0001" + "\u0000\u0000\u0000\u0179\u017a\u0001\u0000\u0000\u0000\u017a\u017c\u0001" + "\u0000\u0000\u0000\u017b\u0179\u0001\u0000\u0000\u0000\u017c\u017d\u0005" + "5\u0000\u0000\u017d7\u0001\u0000\u0000\u0000\u017e\u017f\u00058\u0000" + "\u0000\u017f\u0180\u0003\u000e\u0007\u0000\u0180\u0181\u00059\u0000\u0000" + "\u01819\u0001\u0000\u0000\u0000\u0182\u0183\u0007\u0005\u0000\u0000\u0183" + ";\u0001\u0000\u0000\u0000,QVbdqx\u0088\u008d\u0091\u0096\u009a\u00a1\u00a6" + "\u00a9\u00ad\u00b0\u00b4\u00b7\u00bd\u00c0\u00c2\u00c9\u00d1\u00d6\u00da" + "\u00dc\u00e5\u00ea\u00ed\u00f5\u00fd\u0105\u0107\u0119\u0129\u0137\u013c" + "\u0143\u0149\u015a\u015c\u0161\u016a\u0179"; + public static final ATN _ATN = new ATNDeserializer().deserialize(_serializedATN.toCharArray()); + + static { + _decisionToDFA = new DFA[_ATN.getNumberOfDecisions()]; + for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) { + _decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i); + } + } +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionVisitor.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionVisitor.java new file mode 100644 index 0000000..dbdda44 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/expr/AlertExpressionVisitor.java @@ -0,0 +1,354 @@ +/* + * 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. + */ + +// Generated from AlertExpression.g4 by ANTLR 4.13.2 + +package org.apache.hertzbeat.alert.expr; + +import org.antlr.v4.runtime.tree.ParseTreeVisitor; + +/** + * This interface defines a complete generic visitor for a parse tree produced + * by {@link AlertExpressionParser}. + * + * @param <T> The return type of the visit operation. Use {@link Void} for + * operations with no return type. + */ +public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> { + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#expression}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitExpression(AlertExpressionParser.ExpressionContext ctx); + + /** + * Visit a parse tree produced by the {@code AndExpr} + * labeled alternative in {@link AlertExpressionParser#expr}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitAndExpr(AlertExpressionParser.AndExprContext ctx); + + /** + * Visit a parse tree produced by the {@code ComparisonExpr} + * labeled alternative in {@link AlertExpressionParser#expr}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx); + + /** + * Visit a parse tree produced by the {@code UnlessExpr} + * labeled alternative in {@link AlertExpressionParser#expr}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx); + + /** + * Visit a parse tree produced by the {@code SqlExpr} + * labeled alternative in {@link AlertExpressionParser#expr}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitSqlExpr(AlertExpressionParser.SqlExprContext ctx); + + /** + * Visit a parse tree produced by the {@code SqlCallExpr} + * labeled alternative in {@link AlertExpressionParser#expr}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx); + + /** + * Visit a parse tree produced by the {@code LiteralExpr} + * labeled alternative in {@link AlertExpressionParser#expr}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx); + + /** + * Visit a parse tree produced by the {@code ParenExpr} + * labeled alternative in {@link AlertExpressionParser#expr}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitParenExpr(AlertExpressionParser.ParenExprContext ctx); + + /** + * Visit a parse tree produced by the {@code PromqlCallExpr} + * labeled alternative in {@link AlertExpressionParser#expr}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx); + + /** + * Visit a parse tree produced by the {@code PromqlExpr} + * labeled alternative in {@link AlertExpressionParser#expr}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx); + + /** + * Visit a parse tree produced by the {@code OrExpr} + * labeled alternative in {@link AlertExpressionParser#expr}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitOrExpr(AlertExpressionParser.OrExprContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#functionCall}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#parameterList}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitParameterList(AlertExpressionParser.ParameterListContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#parameter}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitParameter(AlertExpressionParser.ParameterContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#number}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitNumber(AlertExpressionParser.NumberContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#string}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitString(AlertExpressionParser.StringContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#duration}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitDuration(AlertExpressionParser.DurationContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#functionName}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#selectSql}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#selectFieldList}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#selectField}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitSelectField(AlertExpressionParser.SelectFieldContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#groupByList}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitGroupByList(AlertExpressionParser.GroupByListContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#orderByList}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitOrderByList(AlertExpressionParser.OrderByListContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#orderByField}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#limitClause}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#relList}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitRelList(AlertExpressionParser.RelListContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#relation}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitRelation(AlertExpressionParser.RelationContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#conditionList}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitConditionList(AlertExpressionParser.ConditionListContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#compOp}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitCompOp(AlertExpressionParser.CompOpContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#condition}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitCondition(AlertExpressionParser.ConditionContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#conditionUnit}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#promql}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitPromql(AlertExpressionParser.PromqlContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#metricSelector}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherList}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherItem}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherOp}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#labelList}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitLabelList(AlertExpressionParser.LabelListContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#instantVectorOp}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx); + + /** + * Visit a parse tree produced by {@link AlertExpressionParser#aggregationOperator}. + * + * @param ctx the parse tree + * @return the visitor result + */ + T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx); +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatch.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatch.java new file mode 100644 index 0000000..b294e81 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatch.java @@ -0,0 +1,148 @@ +/* + * 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.notice; + +import com.google.common.collect.Maps; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.RejectedExecutionException; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.AlerterWorkerPool; +import org.apache.hertzbeat.alert.config.AlertSseManager; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +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.common.util.JsonUtil; +import org.apache.hertzbeat.plugin.PostAlertPlugin; +import org.apache.hertzbeat.plugin.Plugin; +import org.apache.hertzbeat.plugin.runner.PluginRunner; +import org.springframework.stereotype.Component; + +/** + * Alarm information storage and distribution + */ +@Component +@Slf4j +public class AlertNoticeDispatch { + + private final AlerterWorkerPool workerPool; + private final NoticeConfigService noticeConfigService; + private final AlertStoreHandler alertStoreHandler; + private final Map<Byte, AlertNotifyHandler> alertNotifyHandlerMap; + private final PluginRunner pluginRunner; + private final AlertSseManager emitterManager; + + public AlertNoticeDispatch(AlerterWorkerPool workerPool, + NoticeConfigService noticeConfigService, + AlertStoreHandler alertStoreHandler, + List<AlertNotifyHandler> alertNotifyHandlerList, PluginRunner pluginRunner, AlertSseManager emitterManager) { + this.workerPool = workerPool; + this.noticeConfigService = noticeConfigService; + this.alertStoreHandler = alertStoreHandler; + this.pluginRunner = pluginRunner; + alertNotifyHandlerMap = Maps.newHashMapWithExpectedSize(alertNotifyHandlerList.size()); + this.emitterManager = emitterManager; + alertNotifyHandlerList.forEach(r -> alertNotifyHandlerMap.put(r.type(), r)); + } + + /** + * send alert msg to receiver + * + * @param receiver receiver + * @param alert alert msg + * @return send success or failed + */ + public boolean sendNoticeMsg(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + if (receiver == null || receiver.getType() == null) { + log.warn("DispatcherAlarm-sendNoticeMsg params is empty alert:[{}], receiver:[{}]", alert, receiver); + return false; + } + byte type = receiver.getType(); + if (alertNotifyHandlerMap.containsKey(type)) { + AlertNotifyHandler alertNotifyHandler = alertNotifyHandlerMap.get(type); + if (noticeTemplate == null) { + noticeTemplate = noticeConfigService.getDefaultNoticeTemplateByType(alertNotifyHandler.type()); + } + if (noticeTemplate == null && alertNotifyHandler.type() != 0) { + log.error("alert does not have mapping default notice template. type: {}.", alertNotifyHandler.type()); + throw new NullPointerException(alertNotifyHandler.type() + " does not have mapping default notice template"); + } + alertNotifyHandler.send(receiver, noticeTemplate, alert); + return true; + } + return false; + } + + private NoticeReceiver getOneReceiverById(Long id) { + return noticeConfigService.getReceiverById(id); + } + + private NoticeTemplate getOneTemplateById(Long id) { + if (id == null) { + return null; + } + return noticeConfigService.getOneTemplateById(id); + } + + private Optional<List<NoticeRule>> matchNoticeRulesByAlert(GroupAlert alert) { + return Optional.ofNullable(noticeConfigService.getReceiverFilterRule(alert)); + } + + public void dispatchAlarm(GroupAlert groupAlert) { + if (groupAlert != null) { + // Determining alarm type storage + GroupAlert storedGroupAlert = alertStoreHandler.store(groupAlert); + // Notice distribution + sendNotify(storedGroupAlert); + // Execute the plugin if enable (Compatible with old version plugins, will be removed in later versions) + pluginRunner.pluginExecute(Plugin.class, plugin -> plugin.alert(storedGroupAlert)); + // Execute the plugin if enable with params + pluginRunner.pluginExecute(PostAlertPlugin.class, (afterAlertPlugin, pluginContext) -> afterAlertPlugin.execute(storedGroupAlert, pluginContext)); + // Send alert to the sse client + emitterManager.broadcast(JsonUtil.toJson(storedGroupAlert)); + } + } + + private void sendNotify(GroupAlert alert) { + matchNoticeRulesByAlert(alert).ifPresent(noticeRules -> noticeRules.forEach(rule -> { + NoticeTemplate noticeTemplate = getOneTemplateById(rule.getTemplateId()); + rule.getReceiverId().forEach(receiverId -> { + NoticeReceiver receiver = getOneReceiverById(receiverId); + if (receiver == null || receiver.getType() == null) { + log.warn("DispatchTask skip invalid receiver, receiverId: {}, alertId: {}", receiverId, alert.getId()); + return; + } + try { + workerPool.executeNotify(receiver.getType(), () -> { + try { + sendNoticeMsg(receiver, noticeTemplate, alert); + } catch (AlertNoticeException e) { + log.warn("DispatchTask sendNoticeMsg error, message: {}", e.getMessage()); + } + }); + } catch (RejectedExecutionException e) { + log.warn("DispatchTask rejected notify task, receiverId: {}, type: {}, message: {}", + receiverId, receiver.getType(), e.getMessage()); + } + }); + })); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeException.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeException.java new file mode 100644 index 0000000..319d61c --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeException.java @@ -0,0 +1,27 @@ +/* + * 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.notice; + +/** + * alert notice send failed + */ +public class AlertNoticeException extends RuntimeException { + public AlertNoticeException(String message) { + super(message); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNotifyHandler.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNotifyHandler.java new file mode 100644 index 0000000..3ab8945 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNotifyHandler.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.notice; + +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; + +/** + * AlertNotifyHandler interface + */ +public interface AlertNotifyHandler { + + /** + * send alarm notification + * @param receiver Notification configuration information + * @param noticeTemplate Notification configuration information + * @param alert Alarm information + * @throws AlertNoticeException when send receiver error + */ + void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException; + + /** + * notification type + * @return notification type + */ + byte type(); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertStoreHandler.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertStoreHandler.java new file mode 100644 index 0000000..8ef4054 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertStoreHandler.java @@ -0,0 +1,36 @@ +/* + * 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.notice; + +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; + +/** + * Alarm persistence + */ +public interface AlertStoreHandler { + + /** + * Persistent alarm records + * It is necessary to associate and assign values + * to the alert tag information tags while persisting. + * + * @param alert alarm information + * @return groupAlert + */ + GroupAlert store(GroupAlert alert); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/AbstractAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/AbstractAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..a37b9a2 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/AbstractAlertNotifyHandlerImpl.java @@ -0,0 +1,121 @@ +/* + * 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.notice.impl; + +import freemarker.cache.StringTemplateLoader; +import freemarker.core.TemplateClassResolver; +import freemarker.template.Configuration; +import freemarker.template.TemplateException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.support.event.SystemConfigChangeEvent; +import org.apache.hertzbeat.common.util.ResourceBundleUtil; +import org.apache.hertzbeat.alert.notice.AlertNotifyHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; +import org.springframework.web.client.RestTemplate; + +/** + * Abstract alert notify handler implementation. + */ +@Slf4j +abstract class AbstractAlertNotifyHandlerImpl implements AlertNotifyHandler { + + private static final String NUMBER_FORMAT = "0"; + protected ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter"); + @Autowired + protected RestTemplate restTemplate; + @Autowired + protected AlerterProperties alerterProperties; + + + protected String renderContent(NoticeTemplate noticeTemplate, GroupAlert alert) throws TemplateException, IOException { + StringTemplateLoader stringLoader = new StringTemplateLoader(); + freemarker.template.Template templateRes; + Configuration cfg = new Configuration(Configuration.VERSION_2_3_0); + cfg.setNumberFormat(NUMBER_FORMAT); + cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER); + Map<String, Object> model = new HashMap<>(16); + model.put("title", bundle.getString("alerter.notify.title")); + model.put("status", alert.getStatus()); + model.put("groupLabels", alert.getGroupLabels()); + model.put("commonLabels", alert.getCommonLabels()); + model.put("commonAnnotations", alert.getCommonAnnotations()); + model.put("alerts", alert.getAlerts()); + if (alerterProperties != null) { + model.put("consoleUrl", alerterProperties.getConsoleUrl()); + } + // TODO Single instance reuse cache considers multiple-threading issues + String templateName = "freeMakerTemplate"; + stringLoader.putTemplate(templateName, noticeTemplate.getContent()); + cfg.setTemplateLoader(stringLoader); + templateRes = cfg.getTemplate(templateName, Locale.CHINESE); + String template = FreeMarkerTemplateUtils.processTemplateIntoString(templateRes, model); + return template.replaceAll("((\r\n)|\n)[\\s\t ]*(\\1)+", "$1"); + } + + protected String escapeJsonStr(String jsonStr){ + if (jsonStr == null) { + return null; + } + + StringBuilder sb = new StringBuilder(); + for (char c : jsonStr.toCharArray()) { + switch (c) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + default: + sb.append(c); + } + } + return sb.toString(); + } + + @EventListener(SystemConfigChangeEvent.class) + public void onEvent(SystemConfigChangeEvent event) { + log.info("{} receive system config change event: {}.", this.getClass().getName(), event.getSource()); + this.bundle = ResourceBundleUtil.getBundle("alerter"); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/CommonRobotNotifyResp.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/CommonRobotNotifyResp.java new file mode 100644 index 0000000..f6a91ef --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/CommonRobotNotifyResp.java @@ -0,0 +1,42 @@ +/* + * 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.notice.impl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * common robot http response entity + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CommonRobotNotifyResp { + + @JsonProperty(value = "errcode") + private Integer errCode; + + @JsonProperty(value = "errmsg") + private String errMsg; + + private Integer code; + + private String msg; +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/DbAlertStoreHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/DbAlertStoreHandlerImpl.java new file mode 100644 index 0000000..24fd7d1 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/DbAlertStoreHandlerImpl.java @@ -0,0 +1,135 @@ +/* + * 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.notice.impl; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.GroupAlertDao; +import org.apache.hertzbeat.alert.dao.SingleAlertDao; +import org.apache.hertzbeat.alert.notice.AlertStoreHandler; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.springframework.stereotype.Component; + +/** + * Alarm data persistence - landing in the database + */ +@Component +@RequiredArgsConstructor +@Slf4j +final class DbAlertStoreHandlerImpl implements AlertStoreHandler { + + private final GroupAlertDao groupAlertDao; + + private final SingleAlertDao singleAlertDao; + + @Override + public GroupAlert store(GroupAlert groupAlert) { + if (groupAlert == null || groupAlert.getAlerts() == null || groupAlert.getAlerts().isEmpty()) { + log.error("The Group Alerts is empty, ignore store"); + return groupAlert; + } + // Process individual alerts + Set<String> alertFingerprints = new HashSet<>(8); + List<SingleAlert> originalAlerts = groupAlert.getAlerts(); + List<SingleAlert> newAlerts = new ArrayList<>(); + + for (SingleAlert singleAlert : originalAlerts) { + synchronized (singleAlert.getFingerprint().intern()) { + SingleAlert existAlert = singleAlertDao.findByFingerprint(singleAlert.getFingerprint()); + if (existAlert != null) { + // Update the existing alert with the ID and creation time from the database + singleAlert.setId(existAlert.getId()); + singleAlert.setGmtCreate(existAlert.getGmtCreate()); + // Status transition logic + if (CommonConstants.ALERT_STATUS_FIRING.equals(singleAlert.getStatus())) { + // If the alert is firing and the existing alert is not resolved, update the start time and trigger times + if (!CommonConstants.ALERT_STATUS_RESOLVED.equals(existAlert.getStatus())) { + singleAlert.setStartAt(existAlert.getStartAt()); + int triggerTimes = Optional.ofNullable(existAlert.getTriggerTimes()).orElse(1) + + Optional.ofNullable(singleAlert.getTriggerTimes()).orElse(1); + singleAlert.setTriggerTimes(triggerTimes); + } + } else if (CommonConstants.ALERT_STATUS_RESOLVED.equals(singleAlert.getStatus())) { + // If the alert is resolved, set the end time (if not already set) and copy other fields from the existing alert + if (singleAlert.getEndAt() == null) { + singleAlert.setEndAt(System.currentTimeMillis()); + } + singleAlert.setStartAt(existAlert.getStartAt()); + singleAlert.setActiveAt(existAlert.getActiveAt()); + singleAlert.setTriggerTimes(existAlert.getTriggerTimes()); + } + } + SingleAlert savedSingleAlert = singleAlertDao.save(singleAlert); + newAlerts.add(savedSingleAlert); + alertFingerprints.add(savedSingleAlert.getFingerprint()); + } + } + groupAlert.setAlerts(newAlerts); + // Find existing alert group + synchronized (groupAlert.getGroupKey().intern()) { + GroupAlert existGroupAlert = groupAlertDao.findByGroupKey(groupAlert.getGroupKey()); + // Process resolved alerts + if (existGroupAlert != null) { + List<String> existFingerprints = existGroupAlert.getAlertFingerprints(); + if (existFingerprints != null) { + alertFingerprints.addAll(existFingerprints); + } + // Merge group information + groupAlert.setId(existGroupAlert.getId()); + groupAlert.setGmtCreate(existGroupAlert.getGmtCreate()); + // Merge other historical information to preserve + Map<String, String> existCommonLabels = existGroupAlert.getCommonLabels(); + if (existCommonLabels != null) { + Map<String, String> commonLabels = groupAlert.getCommonLabels(); + if (commonLabels != null) { + // filter common label in commonLabels and existCommonLabels + commonLabels = commonLabels.entrySet().stream() + .filter(entry -> existCommonLabels.containsKey(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + groupAlert.setCommonLabels(commonLabels); + } + } + Map<String, String> existCommonAnnotations = existGroupAlert.getCommonAnnotations(); + if (existCommonAnnotations != null) { + Map<String, String> commonAnnotations = groupAlert.getCommonAnnotations(); + if (commonAnnotations != null) { + // filter common annotation in commonAnnotations and existCommonAnnotations + commonAnnotations = commonAnnotations.entrySet().stream() + .filter(entry -> existCommonAnnotations.containsKey(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + groupAlert.setCommonAnnotations(commonAnnotations); + } + } + } + // Save alert group + groupAlert.setAlertFingerprints(alertFingerprints.stream().toList()); + GroupAlert savedGroupAlert = groupAlertDao.save(groupAlert); + savedGroupAlert.setAlerts(groupAlert.getAlerts()); + return savedGroupAlert; + } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/DingTalkRobotAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/DingTalkRobotAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..b4e4b26 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/DingTalkRobotAlertNotifyHandlerImpl.java @@ -0,0 +1,209 @@ +/* + * 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.notice.impl; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.alert.util.CryptoUtils; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.util.StrUtil; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +/** + * Send alarm information through DingTalk robot + */ +@Component +@RequiredArgsConstructor +@Slf4j +final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + try { + DingTalkWebHookDto dingTalkWebHookDto = new DingTalkWebHookDto(); + DingTalkWebHookDto.MarkdownDTO markdownDTO = new DingTalkWebHookDto.MarkdownDTO(); + markdownDTO.setText(renderContent(noticeTemplate, alert)); + markdownDTO.setTitle(bundle.getString("alerter.notify.title")); + dingTalkWebHookDto.setMarkdown(markdownDTO); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity<DingTalkWebHookDto> httpEntity = new HttpEntity<>(dingTalkWebHookDto, headers); + StringBuilder webHookUrlBuilder = new StringBuilder() + .append(alerterProperties.getDingTalkWebhookUrl()) + .append(receiver.getAccessToken()); + if (StringUtils.isNotBlank(receiver.getAppSecret())) { + webHookUrlBuilder.append(signSecret(receiver.getAppSecret())); + } + String webHookUrl = webHookUrlBuilder.toString(); + ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl, + httpEntity, CommonRobotNotifyResp.class); + if (responseEntity.getStatusCode() == HttpStatus.OK) { + assert responseEntity.getBody() != null; + if (responseEntity.getBody().getErrCode() == 0) { + log.debug("Send dingTalk webHook: {} Success", webHookUrl); + DingTalkWebHookDto dingTalkWebHookTextDto = checkNeedAtNominator(receiver); + if (dingTalkWebHookTextDto != null) { + HttpEntity<DingTalkWebHookDto> httpEntityText = new HttpEntity<>(dingTalkWebHookTextDto, headers); + restTemplate.postForEntity(webHookUrl, + httpEntityText, CommonRobotNotifyResp.class); + } + log.debug("Send dingTalk @ message webHook: {} Success", webHookUrl); + } else { + log.warn("Send dingTalk webHook: {} Failed: {}", webHookUrl, responseEntity.getBody().getErrMsg()); + throw new AlertNoticeException(responseEntity.getBody().getErrMsg()); + } + } else { + log.warn("Send dingTalk webHook: {} Failed: {}", webHookUrl, responseEntity.getBody()); + throw new AlertNoticeException("Http StatusCode " + responseEntity.getStatusCode()); + } + } catch (Exception e) { + throw new AlertNoticeException("[DingTalk Notify Error] " + e.getMessage()); + } + } + + + private DingTalkWebHookDto checkNeedAtNominator(NoticeReceiver receiver) { + if (StringUtils.isBlank(receiver.getPhone()) && StringUtils.isBlank(receiver.getTgUserId())) { + return null; + } + DingTalkWebHookDto dingTalkWebHookDto = new DingTalkWebHookDto(); + dingTalkWebHookDto.setMsgType(DingTalkWebHookDto.TEXT_MSG_TYPE); + DingTalkWebHookDto.AtDTO atDTO = new DingTalkWebHookDto.AtDTO(); + dingTalkWebHookDto.setAt(atDTO); + if (StringUtils.isNotBlank(receiver.getPhone())) { + atDTO.setAtMobiles(StrUtil.analysisArgToList(receiver.getPhone())); + } + if (StringUtils.isNotBlank(receiver.getTgUserId())) { + atDTO.setAtUserIds(StrUtil.analysisArgToList(receiver.getTgUserId())); + } + DingTalkWebHookDto.TextDTO textDTO = new DingTalkWebHookDto.TextDTO(); + textDTO.setContent(bundle.getString("alerter.notify.title")); + dingTalkWebHookDto.setText(textDTO); + return dingTalkWebHookDto; + } + + private String signSecret(String secret) throws Exception { + Long timestamp = System.currentTimeMillis(); + String sign = URLEncoder.encode(CryptoUtils.hmacSha256Base64(secret, timestamp + "\n" + secret), + StandardCharsets.UTF_8); + return String.format("×tamp=%s&sign=%s", timestamp, sign); + } + + @Override + public byte type() { + return 5; + } + + /** + * DingTalk robot request body + * + * @version 1.0 + */ + @Data + private static class DingTalkWebHookDto { + + private static final String DEFAULT_MSG_TYPE = "markdown"; + + /** + * text format + */ + private static final String TEXT_MSG_TYPE = "text"; + + /** + * Message type + */ + @JsonProperty(value = "msgtype") + private String msgType = DEFAULT_MSG_TYPE; + + /** + * markdown message + */ + private MarkdownDTO markdown; + + /** + * @ UserId list + */ + private AtDTO at; + + /** + * text message + */ + private TextDTO text; + + @Data + private static class TextDTO { + /** + * Message content + */ + private String content; + + } + + @Data + private static class MarkdownDTO { + /** + * Message content + */ + private String text; + + /** + * Message title + */ + private String title; + } + + + @Data + private static class AtDTO { + /** + * Message content + */ + private Boolean isAtAll; + + /** + * @ UserId list + */ + private List<String> atUserIds; + + /** + * @ Mobiles list + */ + private List<String> atMobiles; + + } + + } + + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/DiscordBotAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/DiscordBotAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..f257581 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/DiscordBotAlertNotifyHandlerImpl.java @@ -0,0 +1,104 @@ +/* + * 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.notice.impl; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +/** + * Send alarm information through Discord robot + */ +@Component +@RequiredArgsConstructor +@Slf4j +final class DiscordBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException { + try { + var notifyBody = DiscordNotifyDTO.builder() + .embeds(List.of(EmbedDTO.builder() + .title("[" + bundle.getString("alerter.notify.title") + "]") + .description(renderContent(noticeTemplate, alert)) + .build())) + .build(); + var url = String.format(alerterProperties.getDiscordWebhookUrl(), receiver.getDiscordChannelId()); + var headers = new HttpHeaders(); + headers.add("Authorization", "Bot " + receiver.getDiscordBotToken()); + headers.setContentType(MediaType.APPLICATION_JSON); + var request = new HttpEntity<>(notifyBody, headers); + var entity = restTemplate.postForEntity(url, request, DiscordResponseDTO.class); + if (entity.getStatusCode() == HttpStatus.OK && entity.getBody() != null) { + var body = entity.getBody(); + if (body.id != null) { + log.debug("Send Discord Bot Success"); + } else { + log.warn("Send Discord Bot Failed: {}, error_code: {}", body.code, body.message); + throw new AlertNoticeException(body.message); + } + } else { + log.warn("Send Discord Bot Failed {}", entity.getBody()); + throw new AlertNoticeException("Http StatusCode " + entity.getStatusCode()); + } + } catch (Exception e) { + throw new AlertNoticeException("[Discord Bot Notify Error] " + e.getMessage()); + } + } + + @Override + public byte type() { + return 9; + } + + @Data + @Builder + private static class DiscordNotifyDTO { + private List<EmbedDTO> embeds; + } + + @Data + @Builder + private static class EmbedDTO { + private String title; + private String description; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class DiscordResponseDTO { + private String id; + private Integer type; + private String content; + private String message; + private Integer code; + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/EmailAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/EmailAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..f2d8f7e --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/EmailAlertNotifyHandlerImpl.java @@ -0,0 +1,147 @@ +/* + * 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.notice.impl; + +import jakarta.mail.internet.MimeMessage; +import java.util.Date; +import java.util.Properties; +import java.util.ResourceBundle; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.MailServerConfig; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.support.event.SystemConfigChangeEvent; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.common.util.ResourceBundleUtil; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +/** + * Send alarm information through email + */ +@Component +@Slf4j +public class EmailAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + + private final JavaMailSender javaMailSender; + + @Value("${spring.mail.host:smtp.demo.com}") + private String host; + + @Value("${spring.mail.username:demo}") + private String username; + + @Value("${spring.mail.password:demo}") + private String password; + + @Value("${spring.mail.port:465}") + private Integer port; + + @Value("${spring.mail.properties.mail.smtp.ssl.enable:true}") + private boolean sslEnable = true; + + @Value("${spring.mail.properties.mail.smtp.starttls.enable:false}") + private boolean starttlsEnable = false; + + private final GeneralConfigDao generalConfigDao; + + private static final String TYPE = "email"; + + private ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter"); + + public EmailAlertNotifyHandlerImpl(JavaMailSender javaMailSender, GeneralConfigDao generalConfigDao) { + this.javaMailSender = javaMailSender; + this.generalConfigDao = generalConfigDao; + } + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException { + try { + // get sender + JavaMailSenderImpl sender = (JavaMailSenderImpl) javaMailSender; + String fromUsername = username; + try { + boolean useDatabase = false; + GeneralConfig emailConfig = generalConfigDao.findByType(TYPE); + if (emailConfig != null && emailConfig.getContent() != null) { + // enable database configuration + String content = emailConfig.getContent(); + MailServerConfig emailNoticeSenderConfig = JsonUtil.fromJson(content, MailServerConfig.class); + if (emailNoticeSenderConfig != null && emailNoticeSenderConfig.isEnable()) { + sender.setHost(emailNoticeSenderConfig.getEmailHost()); + sender.setPort(emailNoticeSenderConfig.getEmailPort()); + sender.setUsername(emailNoticeSenderConfig.getEmailUsername()); + sender.setPassword(emailNoticeSenderConfig.getEmailPassword()); + Properties props = sender.getJavaMailProperties(); + props.put("mail.smtp.ssl.enable", emailNoticeSenderConfig.isEmailSsl()); + props.put("mail.smtp.starttls.enable", emailNoticeSenderConfig.isEmailStarttls()); + fromUsername = emailNoticeSenderConfig.getEmailUsername(); + useDatabase = true; + } + } + if (!useDatabase) { + // if the database is not configured, use the yml configuration + sender.setHost(host); + sender.setPort(port); + sender.setUsername(username); + sender.setPassword(password); + Properties props = sender.getJavaMailProperties(); + props.put("mail.smtp.ssl.enable", sslEnable); + props.put("mail.smtp.starttls.enable", starttlsEnable); + } + } catch (Exception e) { + log.error("Type not found {}", e.getMessage()); + } + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); + messageHelper.setSubject(bundle.getString("alerter.notify.title")); + // Set sender Email + messageHelper.setFrom(fromUsername); + // Set recipient Email + messageHelper.setTo(receiver.getEmail()); + messageHelper.setSentDate(new Date()); + // Build email templates + String process = renderContent(noticeTemplate, alert); + // Set Email Content Template + messageHelper.setText(process, true); + javaMailSender.send(mimeMessage); + } catch (Exception e) { + throw new AlertNoticeException("[Email Notify Error] " + e.getMessage()); + } + } + + @Override + public byte type() { + return 1; + } + + @Override + @EventListener(SystemConfigChangeEvent.class) + public void onEvent(SystemConfigChangeEvent event) { + log.info("{} receive system config change event: {}.", this.getClass().getName(), event.getSource()); + this.bundle = ResourceBundleUtil.getBundle("alerter"); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..51f961b --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java @@ -0,0 +1,484 @@ +/* + * 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.notice.impl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; +import tools.jackson.databind.JsonNode; + +/** + * FeiShu app alert notify impl + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class FeiShuAppAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + + /** + * get tenant access_token url + */ + private static final String TENANT_ACCESS_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"; + + /** + * get FeiShu app employee url + */ + private static final String EMPLOYEE_URL = "https://open.feishu.cn/open-apis/ehr/v1/employees?status=2&status=4&user_id_type=user_id&page_size=100"; + + /** + * send FeiShu app message url + */ + private static final String APP_MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages"; + + /** + * send FeiShu app batch message url + */ + private static final String APP_BATCH_MESSAGE_URL = "https://open.feishu.cn/open-apis/message/v4/batch_send/"; + + private static final byte USER_RECEIVE_TYPE = 0; + private static final byte CHAT_RECEIVE_TYPE = 1; + private static final byte PART_RECEIVE_TYPE = 2; + private static final byte ALL_RECEIVE_TYPE = 3; + + private final RestTemplate restTemplate; + private final AlerterProperties alerterProperties; + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException { + var appId = receiver.getAppId(); + var appSecret = receiver.getAppSecret(); + var larkReceiveIdType = receiver.getLarkReceiveType(); + try { + var accessToken = getAccessToken(appId, appSecret); + var notificationContent = JsonUtil.toJson(renderContent(noticeTemplate, alert)); + JsonNode messageContent = createLarkMessage(receiver, notificationContent); + switch (larkReceiveIdType) { + case USER_RECEIVE_TYPE -> { + String[] userIds = receiver.getUserId().split(","); + if (userIds.length == 1) { + sendLarkMessage(accessToken, "user_id", userIds[0], messageContent); + } else { + sendLarkUserBatchMessage(accessToken, userIds, messageContent); + } + } + case CHAT_RECEIVE_TYPE -> sendLarkMessage(accessToken, "chat_id", receiver.getChatId(), messageContent); + case PART_RECEIVE_TYPE -> + sendLarkDepartmentBatchMessage(accessToken, receiver.getPartyId().split(","), messageContent); + case ALL_RECEIVE_TYPE -> { + List<String> userIds = new ArrayList<>(); + getLarkEmployeeUserIds(accessToken, null, userIds); + sendLarkUserBatchMessage(accessToken, userIds.toArray(new String[0]), messageContent); + } + default -> throw new AlertNoticeException("Invalid larkReceiveIdType: " + larkReceiveIdType); + } + } catch (Exception e) { + throw new AlertNoticeException("[FeiShu App Notify Error] " + e.getMessage()); + } + } + + @Override + public byte type() { + return 14; + } + + /** + * Send FeiShu app message to chat or designated personnel + * + * @param accessToken Tenant access token + * @param receiverIdType FeiShu app send message receiver id type: user_id, chat_id + * @param receiverId FeiShu app user id or chat id + * @param messageContent Message content + * @see <a href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send message</a> + */ + private void sendLarkMessage(String accessToken, String receiverIdType, String receiverId, JsonNode messageContent) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(accessToken); + FeiShuAppMessageDto messageDto = FeiShuAppMessageDto.builder() + .receiveId(receiverId) + .content(escapedCompactJson(messageContent)) + .build(); + HttpEntity<FeiShuAppMessageDto> request = new HttpEntity<>(messageDto, headers); + call(APP_MESSAGE_URL + "?receive_id_type=" + receiverIdType, request, HttpMethod.POST, FeiShuAppResponse.class); + } + + /** + * Send FeiShu app department batch message + * + * @param accessToken Tenant access token + * @param partyIds FeiShu app department ids + * @param messageContent Message content + * @see <a href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send batch message</a> + */ + private void sendLarkDepartmentBatchMessage(String accessToken, String[] partyIds, JsonNode messageContent) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(accessToken); + FeiShuAppBatchMessageDto batchMessageDto = FeiShuAppBatchMessageDto.builder() + .departmentIds(partyIds) + .card(messageContent) + .build(); + HttpEntity<FeiShuAppBatchMessageDto> request = new HttpEntity<>(batchMessageDto, headers); + call(APP_BATCH_MESSAGE_URL, request, HttpMethod.POST, FeiShuAppResponse.class); + } + + /** + * Send FeiShu app batch message to user + * + * @param accessToken Tenant access token + * @param userIds FeiShu app user ids + * @param messageContent Message content + * @see <a href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send batch message</a> + */ + private void sendLarkUserBatchMessage(String accessToken, String[] userIds, JsonNode messageContent) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(accessToken); + FeiShuAppBatchMessageDto batchMessageDto = FeiShuAppBatchMessageDto.builder() + .userIds(userIds) + .card(messageContent) + .build(); + HttpEntity<FeiShuAppBatchMessageDto> request = new HttpEntity<>(batchMessageDto, headers); + FeiShuAppResponse call = call(APP_BATCH_MESSAGE_URL, request, HttpMethod.POST, FeiShuAppResponse.class); + } + + /** + * Get FeiShu app tenant access token + * + * @param appId Unique identifier for the application, obtained after creating the application + * @param appSecret Application key, obtained after creating the application + * @return Tenant access token + * @see <a href="https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal">tenant_access_token</a> + */ + private String getAccessToken(String appId, String appSecret) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + FeiShuAppAccessTokenDto accessTokenRequest = FeiShuAppAccessTokenDto.builder() + .appId(appId) + .appSecret(appSecret) + .build(); + HttpEntity<FeiShuAppAccessTokenDto> request = new HttpEntity<>(accessTokenRequest, headers); + FeiShuAppAccessTokenResponse data = call(TENANT_ACCESS_TOKEN_URL, request, HttpMethod.POST, FeiShuAppAccessTokenResponse.class); + return data.getTenantAccessToken(); + } + + + /** + * Get FeiShu app employee user ids + * + * @param accessToken FeiShu app tenant access token + * @param pageToken Paging marker, left blank for the first request, indicating traversal from scratch; + * When there are more items in the pagination query result, a new page_token will be returned at the same time. + * The next iteration can use this page_token to obtain the query result + * @param userIds Collection for recursive padding + * @see <a href="https://open.feishu.cn/document/server-docs/ehr-v1/list?appId=cli_a999532b1f52900b">https://open.feishu.cn/open-apis/ehr/v1/employees</a> + */ + private void getLarkEmployeeUserIds(String accessToken, String pageToken, List<String> userIds) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(accessToken); + HttpEntity<String> request = new HttpEntity<>(headers); + var url = StringUtils.isNotBlank(pageToken) ? EMPLOYEE_URL + "&page_token=" + pageToken : EMPLOYEE_URL; + FeiShuAppEmployeeResponse employeeResponse = call(url, + request, + HttpMethod.GET, + FeiShuAppEmployeeResponse.class); + if (Objects.equals(employeeResponse.getCode(), 0)) { + userIds.addAll(employeeResponse.getData().getItems().stream().map(FeiShuAppEmployeeResponse.Employee::getUserId).toList()); + var hasMore = employeeResponse.getData().getHasMore(); + if (Boolean.TRUE.equals(hasMore)) { + getLarkEmployeeUserIds(accessToken, employeeResponse.getData().getPageToken(), userIds); + } + } + + } + + private <R extends FeiShuAppResponse, E> R call(String url, HttpEntity<E> request, HttpMethod httpMethod, Class<R> responseType) { + ResponseEntity<R> response = restTemplate.exchange(url, httpMethod, request, responseType); + if (Objects.nonNull(response.getBody()) && !Objects.equals(response.getBody().getCode(), 0)) { + log.warn("Send FeiShu App Error: {}", response.getBody().getMsg()); + throw new AlertNoticeException("Http StatusCode " + response.getStatusCode() + " Error: " + response.getBody().getMsg()); + } + return response.getBody(); + } + + private JsonNode createLarkMessage(NoticeReceiver receiver, String notificationContent) { + String larkCardMessage = """ + { + "schema": "2.0", + "config": { + "update_multi": true, + "locales": [ + "en_us", + "zh_cn" + ], + "style": { + "text_size": { + "normal_v2": { + "default": "normal", + "pc": "normal", + "mobile": "heading" + } + } + } + }, + "body": { + "direction": "vertical", + "padding": "12px 12px 12px 12px", + "elements": [ + { + "tag": "markdown", + "content": "%s", + "i18n_content": { + "en_us": "" + }, + "text_align": "left", + "text_size": "normal_v2", + "margin": "0px 0px 0px 0px" + }, + { + "tag": "hr", + "margin": "0px 0px 0px 0px" + }, + { + "tag": "column_set", + "horizontal_align": "left", + "columns": [ + { + "tag": "column", + "width": "weighted", + "elements": [ + { + "tag": "button", + "text": { + "tag": "plain_text", + "content": "登入控制台", + "i18n_content": { + "en_us": "Login In" + } + }, + "type": "default", + "width": "default", + "size": "medium", + "behaviors": [ + { + "type": "open_url", + "default_url": "%s", + "pc_url": "", + "ios_url": "", + "android_url": "" + } + ] + } + ], + "direction": "horizontal", + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top", + "weight": 1 + } + ], + "margin": "0px 0px 0px 0px" + } + ] + }, + "header": { + "title": { + "tag": "plain_text", + "content": "HertzBeat 告警", + "i18n_content": { + "en_us": "HertzBeat Alarm" + } + }, + "subtitle": { + "tag": "plain_text", + "content": "" + }, + "template": "red", + "padding": "12px 12px 12px 12px" + } + } + """; + Byte larkReceiveIdType = receiver.getLarkReceiveType(); + String userId = receiver.getUserId(); + String atUserElement = ""; + if (Objects.equals(larkReceiveIdType, CHAT_RECEIVE_TYPE) && StringUtils.isNotBlank(userId)) { + atUserElement = "\\n" + Arrays.stream(userId.split(",")) + .map(id -> "<at id=" + id + "></at>") + .collect(Collectors.joining(" ")); + } + + if (notificationContent.startsWith("\"") && notificationContent.endsWith("\"")) { + notificationContent = StringUtils.removeStart(notificationContent, "\""); + notificationContent = StringUtils.removeEnd(notificationContent, "\""); + } + String jsonStr = String.format(larkCardMessage, + notificationContent.replace("\"", "\\\"") + atUserElement, + alerterProperties.getConsoleUrl()); + return JsonUtil.fromJson(jsonStr); + } + + + private String escapedCompactJson(JsonNode json) { + return json.toString(); + } + + /** + * feiShu app response + */ + @Data + protected static class FeiShuAppResponse { + + private Integer code; + + private String msg; + + } + + /** + * FeiShu app message get tenant access token request + */ + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + protected static class FeiShuAppAccessTokenDto { + + @JsonProperty("app_id") + private String appId; + + @JsonProperty("app_secret") + private String appSecret; + } + + + /** + * FeiShu app message get tenant access token response + */ + @EqualsAndHashCode(callSuper = true) + @Data + protected static class FeiShuAppAccessTokenResponse extends FeiShuAppResponse { + + @JsonProperty("tenant_access_token") + private String tenantAccessToken; + } + + /** + * FeiShu app employee response + */ + @EqualsAndHashCode(callSuper = true) + @Data + protected static class FeiShuAppEmployeeResponse extends FeiShuAppResponse { + + private EmployeeResponseData data; + + @Data + private static class EmployeeResponseData { + @JsonProperty("page_token") + private String pageToken; + + @JsonProperty("has_more") + private Boolean hasMore; + + private List<Employee> items; + } + + @Data + private static class Employee { + + @JsonProperty("user_id") + private String userId; + } + } + + + /** + * FeiShu app message request + */ + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + protected static class FeiShuAppMessageDto { + + @JsonProperty("receive_id") + private String receiveId; + + @Builder.Default + @JsonProperty("msg_type") + private String msgType = "interactive"; + + @JsonProperty("content") + private String content; + + @Builder.Default + private final String uuid = UUID.randomUUID().toString(); + + } + + /** + * FeiShu app batch message request + */ + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + protected static class FeiShuAppBatchMessageDto { + + @Builder.Default + @JsonProperty("msg_type") + private final String msgType = "interactive"; + + private JsonNode card; + + @JsonProperty("department_ids") + private String[] departmentIds; + + @JsonProperty("user_ids") + private String[] userIds; + + } + + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..ab61acb --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImpl.java @@ -0,0 +1,210 @@ +/* + * 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.notice.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * Send alert information through FeiShu + */ +@Component +@RequiredArgsConstructor +@Slf4j +final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + + /** + * Title color corresponding to the alarm priority + */ + private static final String[] TITLE_COLOR = {"red", "yellow", "orange"}; + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + try { + String notificationContent = JsonUtil.toJson(renderContent(noticeTemplate, alert)); + // todo priority custom the color + String cardMessage = createLarkMessage(receiver.getUserId(), notificationContent, (byte) 1); + String webHookUrl = alerterProperties.getFlyBookWebhookUrl() + receiver.getAccessToken(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity<String> flyEntity = new HttpEntity<>(cardMessage, headers); + ResponseEntity<CommonRobotNotifyResp> entity = restTemplate.postForEntity(webHookUrl, + flyEntity, CommonRobotNotifyResp.class); + if (entity.getStatusCode() == HttpStatus.OK) { + assert entity.getBody() != null; + if (entity.getBody().getCode() == null || entity.getBody().getCode() == 0) { + log.debug("Send feiShu webHook: {} Success", webHookUrl); + } else { + log.warn("Send feiShu webHook: {} Failed: {}", webHookUrl, entity.getBody().getMsg()); + throw new AlertNoticeException(entity.getBody().getMsg()); + } + } else { + log.warn("Send feiShu webHook: {} Failed: {}", webHookUrl, entity.getBody()); + throw new AlertNoticeException("Http StatusCode " + entity.getStatusCode()); + } + } catch (Exception e) { + throw new AlertNoticeException("[FeiShu Notify Error] " + e.getMessage()); + } + } + + /** + * create a lark notification card message + * + * @param userId at user id + * @param notificationContent notification content + * @param priority priority for alert + * @return message + */ + private String createLarkMessage(String userId, String notificationContent, byte priority) { + String larkCardMessage = """ + { + "msg_type": "interactive", + "card": { + "schema": "2.0", + "config": { + "update_multi": true, + "locales": [ + "en_us", + "zh_cn" + ], + "style": { + "text_size": { + "normal_v2": { + "default": "normal", + "pc": "normal", + "mobile": "heading" + } + } + } + }, + "body": { + "direction": "vertical", + "padding": "12px 12px 12px 12px", + "elements": [ + { + "tag": "markdown", + "content": "%s", + "i18n_content": { + "en_us": "" + }, + "text_align": "left", + "text_size": "normal_v2", + "margin": "0px 0px 0px 0px" + }, + { + "tag": "hr", + "margin": "0px 0px 0px 0px" + }, + { + "tag": "column_set", + "horizontal_align": "left", + "columns": [ + { + "tag": "column", + "width": "weighted", + "elements": [ + { + "tag": "button", + "text": { + "tag": "plain_text", + "content": "登入控制台", + "i18n_content": { + "en_us": "Login In" + } + }, + "type": "default", + "width": "default", + "size": "medium", + "behaviors": [ + { + "type": "open_url", + "default_url": "%s", + "pc_url": "", + "ios_url": "", + "android_url": "" + } + ] + } + ], + "direction": "horizontal", + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top", + "weight": 1 + } + ], + "margin": "0px 0px 0px 0px" + } + ] + }, + "header": { + "title": { + "tag": "plain_text", + "content": "HertzBeat 告警", + "i18n_content": { + "en_us": "HertzBeat Alarm" + } + }, + "subtitle": { + "tag": "plain_text", + "content": "" + }, + "template": "%s", + "padding": "12px 12px 12px 12px" + } + } + } + """; + + String atUserElement = ""; + if (StringUtils.isNotBlank(userId)) { + atUserElement = "\\n" + Arrays.stream(userId.split(",")) + .map(id -> "<at id=" + id + "></at>") + .collect(Collectors.joining(" ")); + } + + if (notificationContent.startsWith("\"") && notificationContent.endsWith("\"")) { + notificationContent = StringUtils.removeStart(notificationContent, "\""); + notificationContent = StringUtils.removeEnd(notificationContent, "\""); + } + + return String.format(larkCardMessage, + notificationContent.replace("\"", "\\\"") + atUserElement, + alerterProperties.getConsoleUrl(), TITLE_COLOR[priority]); + } + + @Override + public byte type() { + return 6; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/GotifyAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/GotifyAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..17dfe3d --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/GotifyAlertNotifyHandlerImpl.java @@ -0,0 +1,107 @@ +/* + * 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.notice.impl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +/** + * Gotify alert notify handler + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class GotifyAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl{ + + /** + * Send alarm notification + * + * @param receiver Notification configuration information + * @param noticeTemplate Notification configuration information + * @param alert Alarm information + * @throws AlertNoticeException when send receiver error + */ + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException { + try { + GotifyWebHookDto gotifyWebHookDto = new GotifyWebHookDto(); + gotifyWebHookDto.setTitle(bundle.getString("alerter.notify.title")); + gotifyWebHookDto.setMessage(renderContent(noticeTemplate, alert)); + GotifyWebHookDto.ClientDisplay clientDisplay = new GotifyWebHookDto.ClientDisplay(); + clientDisplay.setContentType("text/markdown"); + GotifyWebHookDto.Extras extras = new GotifyWebHookDto.Extras(); + extras.setClientDisplay(clientDisplay); + gotifyWebHookDto.setExtras(extras); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity<GotifyWebHookDto> httpEntity = new HttpEntity<>(gotifyWebHookDto, headers); + String webHookUrl = String.format(alerterProperties.getGotifyWebhookUrl(), receiver.getGotifyToken()); + ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl, + httpEntity, CommonRobotNotifyResp.class); + if (responseEntity.getStatusCode() == HttpStatus.OK) { + log.debug("Send Gotify webHook: {} Success", webHookUrl); + } else { + log.warn("Send Gotify webHook: {} Failed: {}", webHookUrl, responseEntity.getBody()); + throw new AlertNoticeException("Http StatusCode " + responseEntity.getStatusCode()); + } + } catch (Exception e) { + throw new AlertNoticeException("[Gotify Notify Error] " + e.getMessage()); + } + } + + /** + * notification type + * + * @return notification type + */ + @Override + public byte type() { + return 13; + } + + @Data + protected static class GotifyWebHookDto { + private String title; + private String message; + private Extras extras; + + @Data + public static class Extras { + @JsonProperty("client::display") + private ClientDisplay clientDisplay; + } + + @Data + public static class ClientDisplay { + private String contentType; + } + + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/HuaweiCloudSmnAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/HuaweiCloudSmnAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..c35a159 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/HuaweiCloudSmnAlertNotifyHandlerImpl.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.notice.impl; + +import com.huaweicloud.sdk.core.auth.BasicCredentials; +import com.huaweicloud.sdk.smn.v2.SmnClient; +import com.huaweicloud.sdk.smn.v2.model.PublishMessageRequest; +import com.huaweicloud.sdk.smn.v2.model.PublishMessageRequestBody; +import com.huaweicloud.sdk.smn.v2.region.SmnRegion; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentHashMap; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.util.ResourceBundleUtil; +import org.springframework.stereotype.Component; + +/** + * Send alarm information through Huawei CloudSmn + */ +@Component +@RequiredArgsConstructor +@Slf4j +@Deprecated +final class HuaweiCloudSmnAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + private final ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter"); + + private final Map<String, SmnClient> smnClientMap = new ConcurrentHashMap<>(); + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + // todo use rest api not sdk + try { + var smnClient = getSmnClient(receiver); + var request = new PublishMessageRequest() + .withTopicUrn(receiver.getSmnTopicUrn()); + var body = new PublishMessageRequestBody() + .withSubject(bundle.getString("alerter.notify.title")) + .withMessage(renderContent(noticeTemplate, alert)); + request.withBody(body); + var response = smnClient.publishMessage(request); + log.debug("huaweiCloud smn alert response: {}", response); + } catch (Exception e) { + throw new AlertNoticeException("[Huawei Cloud Smn Notify Error] " + e.getMessage()); + } + } + + private SmnClient getSmnClient(NoticeReceiver receiver) { + var key = receiver.getSmnProjectId() + receiver.getSmnAk() + receiver.getSmnSk() + receiver.getSmnRegion(); + if (smnClientMap.containsKey(key)) { + return smnClientMap.get(key); + } + var auth = new BasicCredentials() + .withProjectId(receiver.getSmnProjectId()) + .withAk(receiver.getSmnAk()) + .withSk(receiver.getSmnSk()); + + var smnAsyncClient = SmnClient.newBuilder() + .withCredential(auth) + .withRegion(SmnRegion.valueOf(receiver.getSmnRegion())) + .build(); + smnClientMap.put(key, smnAsyncClient); + return smnAsyncClient; + } + + @Override + public byte type() { + return 11; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/NtfyAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/NtfyAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..e1a606f --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/NtfyAlertNotifyHandlerImpl.java @@ -0,0 +1,182 @@ +/* + * 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.notice.impl; + +import java.util.Map; +import java.util.StringJoiner; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +/** + * Send alert notification through ntfy push notification service. + * Supports priority mapping, tags/emoji, click action URL, + * and optional Bearer token authentication for self-hosted ntfy servers. + * + * @see <a href="https://docs.ntfy.sh/publish/">ntfy publish API</a> + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class NtfyAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + + private static final String STATUS_FIRING = "firing"; + private static final String SEVERITY_CRITICAL = "critical"; + private static final String SEVERITY_WARNING = "warning"; + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException { + try { + String content = renderContent(noticeTemplate, alert); + String url = buildNtfyUrl(receiver); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + headers.set("Title", bundle.getString("alerter.notify.title")); + headers.set("Markdown", "yes"); + headers.set("Priority", String.valueOf(mapPriority(alert))); + headers.set("Tags", buildTags(alert)); + + // Set click action to console URL if available + if (alerterProperties != null && alerterProperties.getConsoleUrl() != null + && !alerterProperties.getConsoleUrl().isEmpty()) { + headers.set("Click", alerterProperties.getConsoleUrl()); + } + + // Set Bearer token authentication for self-hosted ntfy servers + String token = receiver.getNtfyToken(); + if (token != null && !token.isEmpty()) { + headers.set("Authorization", "Bearer " + token); + } + + HttpEntity<String> httpEntity = new HttpEntity<>(content, headers); + ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, httpEntity, String.class); + if (responseEntity.getStatusCode() == HttpStatus.OK) { + log.debug("Send ntfy notification to {} Success", url); + } else { + log.warn("Send ntfy notification to {} Failed: {}", url, responseEntity.getBody()); + throw new AlertNoticeException("Http StatusCode " + responseEntity.getStatusCode()); + } + } catch (Exception e) { + throw new AlertNoticeException("[Ntfy Notify Error] " + e.getMessage()); + } + } + + @Override + public byte type() { + return 15; + } + + /** + * Build the ntfy publish URL from receiver configuration. + */ + private String buildNtfyUrl(NoticeReceiver receiver) { + String serverUrl = receiver.getNtfyServerUrl(); + if (serverUrl == null || serverUrl.isEmpty()) { + serverUrl = alerterProperties.getNtfyDefaultServerUrl(); + } + if (serverUrl.endsWith("/")) { + serverUrl = serverUrl.substring(0, serverUrl.length() - 1); + } + return serverUrl + "/" + receiver.getNtfyTopic(); + } + + /** + * Map alert severity to ntfy priority level (1-5). + * <ul> + * <li>5 (max) - critical severity while firing</li> + * <li>4 (high) - warning severity while firing</li> + * <li>3 (default) - info or unknown severity while firing</li> + * <li>2 (low) - resolved alerts</li> + * </ul> + * + * @see <a href="https://docs.ntfy.sh/publish/#message-priority">ntfy message priority</a> + */ + protected int mapPriority(GroupAlert alert) { + String status = alert.getStatus(); + if (!STATUS_FIRING.equalsIgnoreCase(status)) { + return 2; + } + + String severity = extractSeverity(alert); + if (SEVERITY_CRITICAL.equalsIgnoreCase(severity)) { + return 5; + } else if (SEVERITY_WARNING.equalsIgnoreCase(severity)) { + return 4; + } + return 3; + } + + /** + * Build ntfy tags string with emoji based on alert status and severity. + * Tags appear as emoji icons in the notification. + * + * @see <a href="https://docs.ntfy.sh/publish/#tags-emojis">ntfy tags & emojis</a> + */ + protected String buildTags(GroupAlert alert) { + StringJoiner joiner = new StringJoiner(","); + String status = alert.getStatus(); + + if (STATUS_FIRING.equalsIgnoreCase(status)) { + String severity = extractSeverity(alert); + if (SEVERITY_CRITICAL.equalsIgnoreCase(severity)) { + joiner.add("rotating_light"); + joiner.add("skull"); + } else if (SEVERITY_WARNING.equalsIgnoreCase(severity)) { + joiner.add("warning"); + } else { + joiner.add("information_source"); + } + } else { + joiner.add("white_check_mark"); + } + + // Append alert name as a plain-text tag if available + Map<String, String> commonLabels = alert.getCommonLabels(); + if (commonLabels != null && commonLabels.containsKey("alertname")) { + joiner.add(commonLabels.get("alertname")); + } + + return joiner.toString(); + } + + /** + * Extract severity from alert's common labels. + * Checks "severity" key first, then falls back to "priority". + */ + private String extractSeverity(GroupAlert alert) { + Map<String, String> commonLabels = alert.getCommonLabels(); + if (commonLabels == null) { + return null; + } + String severity = commonLabels.get("severity"); + if (severity == null) { + severity = commonLabels.get("priority"); + } + return severity; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/ServerChanAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/ServerChanAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..1b1b7a5 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/ServerChanAlertNotifyHandlerImpl.java @@ -0,0 +1,108 @@ +/* + * 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.notice.impl; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * Send alarm information through Server + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class ServerChanAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + /** + * Send alarm notification + * + * @param receiver Notification configuration information + * @param alert Alarm information + * @throws AlertNoticeException when send receiver error + */ + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException { + try { + ServerChanAlertNotifyHandlerImpl.ServerChanWebHookDto serverChanWebHookDto = new ServerChanAlertNotifyHandlerImpl.ServerChanWebHookDto(); + serverChanWebHookDto.setTitle(bundle.getString("alerter.notify.title")); + serverChanWebHookDto.setDesp(renderContent(noticeTemplate, alert)); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity<ServerChanAlertNotifyHandlerImpl.ServerChanWebHookDto> httpEntity = new HttpEntity<>(serverChanWebHookDto, headers); + String sanitizedToken = receiver.getServerChanToken().replaceAll("[^a-zA-Z0-9_-]", ""); + String webHookUrl = String.format(alerterProperties.getServerChanWebhookUrl(), sanitizedToken); + + // Validate the constructed URL against a whitelist + List<String> allowedBaseUrls = List.of("https://api.serverchan.com", "https://serverchan.example.com", "https://sctapi.ftqq.com"); + boolean isValidUrl = allowedBaseUrls.stream().anyMatch(webHookUrl::startsWith); + if (!isValidUrl) { + throw new AlertNoticeException("Invalid webhook URL: " + webHookUrl); + } + + ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl, + httpEntity, CommonRobotNotifyResp.class); + if (responseEntity.getStatusCode() == HttpStatus.OK) { + log.debug("Send ServerChan webHook: {} Success", webHookUrl); + } else { + log.warn("Send ServerChan webHook: {} Failed: {}", webHookUrl, responseEntity.getBody()); + throw new AlertNoticeException("Http StatusCode " + responseEntity.getStatusCode()); + } + } catch (Exception e) { + throw new AlertNoticeException("[ServerChan Notify Error] " + e.getMessage()); + } + } + + /** + * notification type + * + * @return notification type + */ + @Override + public byte type() { + return 12; + } + + @Data + protected static class ServerChanWebHookDto { + private static final String MARKDOWN = "markdown"; + + /** + * title + */ + private String title; + + /** + * markdown message content + */ + private String desp; + + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/SlackAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/SlackAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..8f78207 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/SlackAlertNotifyHandlerImpl.java @@ -0,0 +1,107 @@ +/* + * 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.notice.impl; + +import java.net.URI; +import java.util.Objects; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +/** + * Send alarm information by Slack Webhook + */ +@Component +@RequiredArgsConstructor +@Slf4j +final class SlackAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + private static final String SUCCESS = "ok"; + private final RestTemplate restTemplate; + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException { + try { + var slackNotify = SlackNotifyDTO.builder() + .text(renderContent(noticeTemplate, alert)) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity<SlackNotifyDTO> slackNotifyEntity = new HttpEntity<>(slackNotify, headers); + String slackWebHookUrl = receiver.getSlackWebHookUrl(); + if (!isValidSlackWebHookUrl(slackWebHookUrl)) { + log.warn("Invalid Slack Webhook URL: {}", slackWebHookUrl); + throw new AlertNoticeException("Invalid Slack Webhook URL"); + } + var entity = restTemplate.postForEntity(slackWebHookUrl, slackNotifyEntity, String.class); + if (entity.getStatusCode() == HttpStatus.OK && entity.getBody() != null) { + var body = entity.getBody(); + if (Objects.equals(SUCCESS, body)) { + log.debug("Send Slack Success"); + } else { + log.warn("Send Slack Failed: {}", body); + throw new AlertNoticeException(body); + } + } else { + log.warn("Send Slack Failed {}", entity.getBody()); + throw new AlertNoticeException("Http StatusCode " + entity.getStatusCode()); + } + } catch (Exception e) { + throw new AlertNoticeException("[Slack Notify Error] " + e.getMessage()); + } + } + + @Override + public byte type() { + return 8; + } + + @Data + @Builder + private static class SlackNotifyDTO { + private String text; + } + + + /** + * Validate if the Slack Webhook URL belongs to an allowed domain. + * + * @param url the Slack Webhook URL to validate + * @return true if the URL is valid, false otherwise + */ + private boolean isValidSlackWebHookUrl(String url) { + try { + URI uri = new URI(url); + String host = uri.getHost(); + return "hooks.slack.com".equals(host); + } catch (Exception e) { + log.warn("Error validating Slack Webhook URL: {}", url, e); + return false; + } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/SmsAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/SmsAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..b1e801b --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/SmsAlertNotifyHandlerImpl.java @@ -0,0 +1,63 @@ +/* + * 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.notice.impl; + +import java.util.ResourceBundle; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.alert.service.SmsClient; +import org.apache.hertzbeat.alert.service.SmsClientFactory; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.util.ResourceBundleUtil; +import org.springframework.stereotype.Component; + +/** + * Send alarm information through Sms + */ +@Component +@RequiredArgsConstructor +@Slf4j +final class SmsAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + + private final SmsClientFactory smsFactory; + private final ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter"); + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + try { + SmsClient smsClient = smsFactory.getSmsClient(); + if (smsClient == null) { + throw new AlertNoticeException("No SMS Service available, please check the configuration"); + } + if (!smsClient.checkConfig()) { + throw new AlertNoticeException(smsClient.getType() + " SMS Service configuration is invalid, please check the configuration"); + } + smsClient.sendMessage(receiver, noticeTemplate, alert); + } catch (Exception e) { + throw new AlertNoticeException("[Sms Notify Error] " + e.getMessage()); + } + } + + @Override + public byte type() { + return 0; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/TelegramBotAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/TelegramBotAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..9573bad --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/TelegramBotAlertNotifyHandlerImpl.java @@ -0,0 +1,114 @@ +/* + * 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.notice.impl; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +/** + * Send alarm information by Telegram Bot + */ +@Component +@Slf4j +final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) + throws AlertNoticeException { + try { + String token = receiver.getTgBotToken(); + if (!isValidTelegramToken(token)) { + throw new AlertNoticeException("Invalid Telegram Bot Token"); + } + String url = String.format(alerterProperties.getTelegramWebhookUrl(), token); + TelegramBotNotifyDTO notifyBody = TelegramBotNotifyDTO.builder() + .chatId(receiver.getTgUserId()) + .text(renderContent(noticeTemplate, alert)) + .messageThreadId(receiver.getTgMessageThreadId()) + .disableWebPagePreview(true) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity<TelegramBotNotifyDTO> telegramEntity = new HttpEntity<>(notifyBody, headers); + ResponseEntity<TelegramBotNotifyResponse> entity = restTemplate.postForEntity(url, telegramEntity, + TelegramBotNotifyResponse.class); + if (entity.getStatusCode() == HttpStatus.OK && entity.getBody() != null) { + TelegramBotNotifyResponse body = entity.getBody(); + if (body.ok) { + log.debug("Send Telegram Bot Success"); + } else { + log.warn("Send Telegram Bot Failed: {}, error_code: {}", body.description, body.errorCode); + throw new AlertNoticeException(body.description); + } + } else { + log.warn("Send Telegram Bot Failed {}", entity.getBody()); + throw new AlertNoticeException("Http StatusCode " + entity.getStatusCode()); + } + } catch (Exception e) { + throw new AlertNoticeException("[Telegram Bot Notify Error] " + e.getMessage()); + } + } + + @Override + public byte type() { + return 7; + } + + @Data + @Builder + private static class TelegramBotNotifyDTO { + @JsonProperty("chat_id") + private String chatId; + private String text; + @JsonProperty("disable_web_page_preview") + private Boolean disableWebPagePreview; + @JsonProperty("message_thread_id") + private String messageThreadId; + } + + @NoArgsConstructor + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class TelegramBotNotifyResponse { + private boolean ok; + @JsonProperty("error_code") + private Integer errorCode; + private String description; + } + + private boolean isValidTelegramToken(String token) { + // Adjusted pattern to match real Telegram Bot tokens like + // 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw + String tokenPattern = "^[0-9]+:[a-zA-Z0-9_-]+$"; + return token != null && token.matches(tokenPattern); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/WeChatAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/WeChatAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..1139f48 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/WeChatAlertNotifyHandlerImpl.java @@ -0,0 +1,112 @@ +/* + * 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.notice.impl; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.http.HttpHeaders; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; + +/** + * Send alarm information through WeChat + */ +final class WeChatAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + private static final Logger log = LoggerFactory.getLogger(WeChatAlertNotifyHandlerImpl.class); + private static final String CORP_ID = "YOUR_CORP_ID"; + private static final String CORP_SECRET = "YOUR_CORP_SECRET"; + private static final String AGENT_ID = "YOUR_AGENT_ID"; + private static final String GET_TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + CORP_ID + "&corpsecret=" + CORP_SECRET; + private static final String SEND_MESSAGE_URL = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="; + private static final String ACCESS_TOKEN = "access_token"; + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + try { + String accessToken = getAccessToken(); + String messageContent = constructMessageContent(receiver, noticeTemplate, alert); + sendMessage(accessToken, messageContent); + } catch (Exception e) { + log.error("Failed to send WeChat alert", e); + } + } + + private String getAccessToken() throws Exception { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(GET_TOKEN_URL)) + .GET() + .build(); + HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); + JsonObject jsonObject = JsonParser.parseString(response.body()).getAsJsonObject(); + String accessToken = null; + if (jsonObject.has(ACCESS_TOKEN)) { + accessToken = jsonObject.get(ACCESS_TOKEN).getAsString(); + } else { + // todo Handle error conditions, such as logging or throwing exceptions + log.error("Failed to obtain ACCESS_TOKEN from response: {}", response.body()); + } + + return accessToken; + } + + private String constructMessageContent(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + // example: construct a text message content + JsonObject messageContent = new JsonObject(); + messageContent.addProperty("msgtype", "text"); + JsonObject textContent = new JsonObject(); + + // Here you can construct the message content based on the NoticeTemplate and Alert information + String alertMessage = "Alert message content"; + textContent.addProperty("content", alertMessage); + messageContent.add("text", textContent); + + // if need @ someone, you can add here + JsonObject atInfo = new JsonObject(); + atInfo.addProperty("isAtAll", false); // Whether @everyone + messageContent.add("at", atInfo); + + // return JSON string + return messageContent.toString(); + } + + private void sendMessage(String accessToken, String messageContent) throws Exception { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(SEND_MESSAGE_URL + accessToken)) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .POST(HttpRequest.BodyPublishers.ofString(messageContent)) + .build(); + HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); + // Check if the message was sent successfully + log.info("Message sent response: {}", response.body()); + } + + @Override + public byte type() { + return 3; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/WeComAppAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/WeComAppAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..2bb671f --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/WeComAppAlertNotifyHandlerImpl.java @@ -0,0 +1,203 @@ +/* + * 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.notice.impl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +/** + * WeChat app alert notify impl + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class WeComAppAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + + /** + * send weChat app message url + */ + private static final String APP_MESSAGE_URL = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s"; + + /** + * get access_token url + */ + private static final String SECRET_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"; + + /** + * app message send object + */ + private static final String DEFAULT_ALL = "@all"; + + private final RestTemplate restTemplate; + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException { + String corpId = receiver.getCorpId(); + Integer agentId = receiver.getAgentId(); + String appSecret = receiver.getAppSecret(); + String userId = receiver.getUserId(); + String partyId = receiver.getPartyId(); + String tagId = receiver.getTagId(); + + + try { + ResponseEntity<WeChatAppReq> entityResponse = restTemplate.getForEntity(String.format(SECRET_URL, corpId, appSecret), WeChatAppReq.class); + if (Objects.nonNull(entityResponse.getBody())) { + String accessToken = entityResponse.getBody().getAccessToken(); + WeChatAppDTO.MarkdownDTO markdown = new WeChatAppDTO.MarkdownDTO(); + markdown.setContent(renderContent(noticeTemplate, alert)); + WeChatAppDTO.WeChatAppDTOBuilder weChatAppDTOBuilder = WeChatAppDTO.builder() + .msgType(WeChatAppDTO.MARKDOWN) + .markdown(markdown) + .agentId(agentId); + boolean hasUserId = receiver.getUserId() != null; + boolean hasPartyId = receiver.getPartyId() != null; + boolean hasTagId = receiver.getTagId() != null; + + if (hasUserId) { + weChatAppDTOBuilder.toUser(userId); + } + if (hasPartyId) { + weChatAppDTOBuilder.toParty(partyId); + } + if (hasTagId) { + weChatAppDTOBuilder.toTag(tagId); + } + + if (!hasUserId && !hasPartyId && !hasTagId) { + weChatAppDTOBuilder.toUser(DEFAULT_ALL); + } + + WeChatAppDTO weChatAppDTO = weChatAppDTOBuilder.build(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity<WeChatAppDTO> weChatAppEntity = new HttpEntity<>(weChatAppDTO, headers); + ResponseEntity<WeChatAppReq> response = restTemplate.postForEntity(String.format(APP_MESSAGE_URL, accessToken), weChatAppEntity, WeChatAppReq.class); + if (Objects.nonNull(response.getBody()) && !Objects.equals(response.getBody().getErrCode(), 0)) { + log.warn("Send Enterprise WeChat App Error: {}", response.getBody().getErrMsg()); + throw new AlertNoticeException("Http StatusCode " + response.getStatusCode() + " Error: " + response.getBody().getErrMsg()); + } + } + } catch (Exception e) { + throw new AlertNoticeException("[Enterprise WeChat Notify Error] " + e.getMessage()); + } + } + + @Override + public byte type() { + return 10; + } + + /** + * WeChat app request + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + protected static class WeChatAppReq { + + @JsonProperty(value = "errcode") + private Integer errCode; + + @JsonProperty(value = "errmsg") + private String errMsg; + + @JsonProperty(value = "access_token") + private String accessToken; + } + + /** + * WeChat app message + */ + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + protected static class WeChatAppDTO { + + /** + * markdown format + */ + public static final String MARKDOWN = "markdown"; + + @JsonProperty(value = "touser") + private String toUser; + + @JsonProperty(value = "toparty") + private String toParty; + + @JsonProperty(value = "totag") + private String toTag; + + @JsonProperty(value = "msgtype") + private String msgType; + + @JsonProperty(value = "agentid") + private Integer agentId; + + /** + * text message + */ + private TextDTO text; + + /** + * markdown message + */ + private MarkdownDTO markdown; + + /** + * markdown message + */ + @Data + protected static class MarkdownDTO { + /** + * message content + */ + private String content; + } + + /** + * text message + */ + @Data + protected static class TextDTO { + /** + * message content + */ + private String content; + } + + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/WeComRobotAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/WeComRobotAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..86d05f3 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/WeComRobotAlertNotifyHandlerImpl.java @@ -0,0 +1,189 @@ +/* + * 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.notice.impl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Objects; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.util.StrUtil; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +/** + * Send alarm information through enterprise WeChat + */ +@Component +@RequiredArgsConstructor +@Slf4j +final class WeComRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + try { + WeWorkWebHookDto weWorkWebHookDTO = new WeWorkWebHookDto(); + WeWorkWebHookDto.MarkdownDTO markdownDTO = new WeWorkWebHookDto.MarkdownDTO(); + markdownDTO.setContent(renderContent(noticeTemplate, alert)); + weWorkWebHookDTO.setMarkdown(markdownDTO); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity<WeWorkWebHookDto> httpEntity = new HttpEntity<>(weWorkWebHookDTO, headers); + String wechatId = receiver.getWechatId(); + if (!isValidWechatId(wechatId)) { + log.warn("Invalid WeChat ID: {}", wechatId); + throw new AlertNoticeException("Invalid WeChat ID provided."); + } + String webHookUrl = alerterProperties.getWeWorkWebhookUrl() + wechatId; + ResponseEntity<CommonRobotNotifyResp> entity = restTemplate.postForEntity(webHookUrl, httpEntity, CommonRobotNotifyResp.class); + if (entity.getStatusCode() == HttpStatus.OK) { + assert entity.getBody() != null; + if (entity.getBody().getErrCode() == 0) { + log.debug("Send WeWork webHook: {} Success", webHookUrl); + WeWorkWebHookDto weWorkWebHookTextDto = checkNeedAtNominator(receiver, alert); + if (!Objects.isNull(weWorkWebHookTextDto)) { + HttpEntity<WeWorkWebHookDto> httpEntityText = new HttpEntity<>(weWorkWebHookTextDto, headers); + restTemplate.postForEntity(webHookUrl, httpEntityText, CommonRobotNotifyResp.class); + } + + } else { + log.warn("Send WeWork webHook: {} Failed: {}", webHookUrl, entity.getBody().getErrMsg()); + throw new AlertNoticeException(entity.getBody().getErrMsg()); + } + } else { + log.warn("Send WeWork webHook: {} Failed: {}", webHookUrl, entity.getBody()); + throw new AlertNoticeException("Http StatusCode " + entity.getStatusCode()); + } + } catch (Exception e) { + throw new AlertNoticeException("[WeWork Notify Error] " + e.getMessage()); + } + } + + private WeWorkWebHookDto checkNeedAtNominator(NoticeReceiver receiver, GroupAlert alert) { + if (StringUtils.isBlank(receiver.getPhone()) && StringUtils.isBlank(receiver.getUserId())) { + return null; + } + WeWorkWebHookDto weWorkWebHookTextDto = new WeWorkWebHookDto(); + weWorkWebHookTextDto.setMsgtype(WeWorkWebHookDto.TEXT_MSG_TYPE); + WeWorkWebHookDto.TextDTO textDto = new WeWorkWebHookDto.TextDTO(); + if (StringUtils.isNotBlank(receiver.getPhone())) { + textDto.setMentionedMobileList(StrUtil.analysisArgToList(receiver.getPhone())); + weWorkWebHookTextDto.setText(textDto); + } + if (StringUtils.isNotBlank(receiver.getUserId())) { + textDto.setMentionedList(StrUtil.analysisArgToList(receiver.getUserId())); + weWorkWebHookTextDto.setText(textDto); + } + return weWorkWebHookTextDto; + + } + + + @Override + public byte type() { + return 4; + } + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + private static class WeWorkWebHookDto { + + public static final String WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key="; + + /** + * default msg type : markdown format + */ + private static final String DEFAULT_MSG_TYPE = "markdown"; + + /** + * text format + */ + private static final String TEXT_MSG_TYPE = "text"; + + /** + * message type + */ + @Builder.Default + private String msgtype = DEFAULT_MSG_TYPE; + + /** + * markdown message + */ + private MarkdownDTO markdown; + + /** + * text message + */ + private TextDTO text; + + @Data + private static class MarkdownDTO { + + /** + * message content + */ + private String content; + } + + @Data + private static class TextDTO { + + /** + * message content + */ + private String content; + /** + * @ userId + */ + @JsonProperty(value = "mentioned_list") + private List<String> mentionedList; + /** + * @ phone + */ + @JsonProperty(value = "mentioned_mobile_list") + private List<String> mentionedMobileList; + } + + } + + /** + * Validate the WeChat ID to ensure it meets the expected format. + * + * @param wechatId the WeChat ID to validate + * @return true if valid, false otherwise + */ + private boolean isValidWechatId(String wechatId) { + // Example validation: ensure the ID is alphanumeric and non-empty + return StringUtils.isNotBlank(wechatId) && wechatId.matches("^[a-zA-Z0-9_-]+$"); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/WebHookAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/WebHookAlertNotifyHandlerImpl.java new file mode 100644 index 0000000..8d60177 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/WebHookAlertNotifyHandlerImpl.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.notice.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +/** + * Send alarm information through WebHookAlter + */ +@Component +@Slf4j +final class WebHookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl { + + @Override + public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + try { + String hookUrl = receiver.getHookUrl(); + + // Basic URL validation + if (StringUtils.isBlank(hookUrl)) { + throw new AlertNoticeException("Webhook URL is null or empty"); + } + + HttpHeaders headers = new HttpHeaders(); + if ("Basic".equalsIgnoreCase(receiver.getHookAuthType())) { + headers.setBasicAuth(receiver.getHookAuthToken()); + } else if ("Bearer".equalsIgnoreCase(receiver.getHookAuthType())) { + headers.setBearerAuth(receiver.getHookAuthToken()); + } + headers.setContentType(MediaType.APPLICATION_JSON); + // alert.setContent(escapeJsonStr(alert.getContent())); + String webhookJson = renderContent(noticeTemplate, alert); + webhookJson = webhookJson.replace(",\n }", "\n }"); + + HttpEntity<String> alertHttpEntity = new HttpEntity<>(webhookJson, headers); + ResponseEntity<String> entity = restTemplate.postForEntity(hookUrl, alertHttpEntity, String.class); + if (entity.getStatusCode().value() < HttpStatus.BAD_REQUEST.value()) { + log.debug("Send WebHook: {} Success", hookUrl); + } else { + log.warn("Send WebHook: {} Failed: {}", hookUrl, entity.getBody()); + throw new AlertNoticeException("Http StatusCode " + entity.getStatusCode()); + } + } catch (Exception e) { + throw new AlertNoticeException("[WebHook Notify Error] " + e.getMessage()); + } + } + + @Override + public byte type() { + return 2; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduce.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduce.java new file mode 100644 index 0000000..75f18f9 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduce.java @@ -0,0 +1,132 @@ +/* + * 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 com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.concurrent.ManagedExecutor; +import org.apache.hertzbeat.common.concurrent.ManagedExecutors; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * common reduce alarm worker + */ +@Service +@Slf4j +public class AlarmCommonReduce implements DisposableBean { + + private final AlarmGroupReduce alarmGroupReduce; + + private final ManagedExecutor workerExecutor; + + public AlarmCommonReduce(AlarmGroupReduce alarmGroupReduce) { + this(alarmGroupReduce, VirtualThreadProperties.defaults()); + } + + @Autowired + public AlarmCommonReduce(AlarmGroupReduce alarmGroupReduce, VirtualThreadProperties virtualThreadProperties) { + this.alarmGroupReduce = alarmGroupReduce; + VirtualThreadProperties properties = + virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties; + this.workerExecutor = initWorkExecutor(properties); + } + + private ManagedExecutor initWorkExecutor(VirtualThreadProperties properties) { + Thread.UncaughtExceptionHandler handler = (thread, throwable) -> { + log.error("alerter-reduce-worker has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }; + if (properties.enabled()) { + VirtualThreadProperties.QueueProperties queueProperties = properties.alerter().reduce(); + return ManagedExecutors.newQueuedVirtualExecutor("alerter-reduce-worker", "alerter-reduce-worker-", + queueProperties.maxConcurrentJobs(), queueProperties.queueCapacity(), handler); + } + return ManagedExecutors.wrap("alerter-reduce-worker", new java.util.concurrent.ThreadPoolExecutor(2, + 2, + 10, + java.util.concurrent.TimeUnit.SECONDS, + new java.util.concurrent.LinkedBlockingQueue<>(), + new ThreadFactoryBuilder() + .setUncaughtExceptionHandler(handler) + .setDaemon(true) + .setNameFormat("alerter-reduce-worker-%d") + .build(), + new java.util.concurrent.ThreadPoolExecutor.AbortPolicy())); + } + + + public void reduceAndSendAlarm(SingleAlert alert) { + workerExecutor.execute(reduceAlarmTask(alert)); + } + + public void reduceAndSendAlarmGroup(Map<String, String> groupLabels, List<SingleAlert> alerts) { + workerExecutor.execute(() -> { + try { + // Generate alert fingerprint + for (SingleAlert alert : alerts) { + String fingerprint = generateAlertFingerprint(alert.getLabels()); + alert.setFingerprint(fingerprint); + } + // Process the group alert + alarmGroupReduce.processGroupAlert(groupLabels, alerts); + } catch (Exception e) { + log.error("Reduce alarm group failed: {}", e.getMessage()); + } + }); + } + + Runnable reduceAlarmTask(SingleAlert alert) { + return () -> { + try { + // Generate alert fingerprint + String fingerprint = generateAlertFingerprint(alert.getLabels()); + alert.setFingerprint(fingerprint); + alarmGroupReduce.processGroupAlert(alert); + } catch (Exception e) { + log.error("Reduce alarm failed: {}", e.getMessage()); + } + }; + } + + /** + * Generate fingerprint for alert to identify duplicates + * Fingerprint is based on labels excluding timestamp related fields + */ + private String generateAlertFingerprint(Map<String, String> labels) { + return labels.entrySet().stream() + .filter(e -> !"timestamp".equals(e.getKey()) + && !"starts_at".equals(e.getKey()) && !"actives_at".equals(e.getKey()) + && !"end_at".equals(e.getKey()) && !"ends_at".equals(e.getKey()) + && !"start_at".equals(e.getKey()) && !"active_at".equals(e.getKey())) + .sorted(Map.Entry.comparingByKey()) + .map(e -> e.getKey() + ":" + e.getValue()) + .collect(Collectors.joining(",")); + } + + @Override + public void destroy() { + workerExecutor.close(); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduce.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduce.java new file mode 100644 index 0000000..597ef9a --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduce.java @@ -0,0 +1,465 @@ +/* + * 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 com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Alarm group reduce + * refer from prometheus, code with @cursor + */ +@Component +@Slf4j +public class AlarmGroupReduce implements DisposableBean { + + /** + * Default initial group wait time 30s + */ + private static final long DEFAULT_GROUP_WAIT = 30 * 1000L; + + /** + * Default group send interval 5min + */ + private static final long DEFAULT_GROUP_INTERVAL = 5 * 60 * 1000L; + + /** + * Default repeat interval 4h + */ + private static final long DEFAULT_REPEAT_INTERVAL = 4 * 60 * 60 * 1000L; + + /** + * Milliseconds per second + */ + private static final long MS_PER_SECOND = 1000L; + + /** + * Check interval for group send ms + */ + private static final long CHECK_INTERVAL = 1000L; + + private final AlarmInhibitReduce alarmInhibitReduce; + + /** + * Group define rules + * key: rule name + * value: group rule configuration + */ + private final Map<String, AlertGroupConverge> groupDefines; + + /** + * Alert cache grouped by labels + * key: groupDefineKey:groupKey + * value: GroupAlertCache + */ + private final Map<String, GroupAlertCache> groupCacheMap; + + private final ScheduledExecutorService scheduledExecutor; + + private final ExecutorService workerExecutor; + + private final ScheduledDispatchTask checkTask; + + public AlarmGroupReduce(AlarmInhibitReduce alarmInhibitReduce, AlertGroupConvergeDao alertGroupConvergeDao) { + this(alarmInhibitReduce, alertGroupConvergeDao, VirtualThreadProperties.defaults(), true); + } + + @Autowired + public AlarmGroupReduce(AlarmInhibitReduce alarmInhibitReduce, AlertGroupConvergeDao alertGroupConvergeDao, + VirtualThreadProperties virtualThreadProperties) { + this(alarmInhibitReduce, alertGroupConvergeDao, virtualThreadProperties, true); + } + + AlarmGroupReduce(AlarmInhibitReduce alarmInhibitReduce, AlertGroupConvergeDao alertGroupConvergeDao, + VirtualThreadProperties virtualThreadProperties, boolean autoStart) { + this.alarmInhibitReduce = alarmInhibitReduce; + this.groupDefines = new ConcurrentHashMap<>(8); + this.groupCacheMap = new ConcurrentHashMap<>(8); + VirtualThreadProperties properties = + virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties; + this.scheduledExecutor = createScheduler(); + this.workerExecutor = createVirtualExecutor(properties); + this.checkTask = new ScheduledDispatchTask(workerExecutor, this::runCheckAndSendGroups); + List<AlertGroupConverge> groupConverges = alertGroupConvergeDao.findAlertGroupConvergesByEnableIsTrue(); + refreshGroupDefines(groupConverges); + if (autoStart) { + startCheckAndSendGroups(); + } + } + + private void startCheckAndSendGroups() { + scheduledExecutor.scheduleAtFixedRate(this::dispatchCheckAndSendGroups, 10000, CHECK_INTERVAL, + TimeUnit.MILLISECONDS); + } + + void dispatchCheckAndSendGroups() { + checkTask.dispatch(); + } + + void beforeCheckAndSendGroupsRun() { + } + + @Override + public void destroy() { + scheduledExecutor.shutdownNow(); + if (workerExecutor != null) { + workerExecutor.shutdownNow(); + } + } + + private ScheduledExecutorService createScheduler() { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("Check alarm groups calculate has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("alarm-group-calculate-%d") + .build(); + return Executors.newSingleThreadScheduledExecutor(threadFactory); + } + + private ExecutorService createVirtualExecutor(VirtualThreadProperties properties) { + if (!properties.enabled()) { + return null; + } + return Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name("alarm-group-calculate-vt-", 0) + .uncaughtExceptionHandler((thread, throwable) -> { + log.error("Check alarm groups calculate worker has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .factory()); + } + + private void runCheckAndSendGroups() { + beforeCheckAndSendGroupsRun(); + try { + long now = System.currentTimeMillis(); + groupCacheMap.forEach((groupKey, cache) -> { + if (shouldSendGroup(cache, now)) { + sendGroupAlert(cache); + cache.setLastSendTime(now); + cache.getAlertFingerprints().clear(); + } + }); + } catch (Exception e) { + log.error("Check alarm groups calculate has exception.: {}", e.getMessage(), e); + } + } + + /** + * Configure group define rules + * @param groupDefines map of group rule configurations + */ + public void refreshGroupDefines(List<AlertGroupConverge> groupDefines) { + this.groupDefines.clear(); + groupDefines.forEach(define -> this.groupDefines.put(define.getName(), define)); + } + + /** + * Process single alert and group by defined rules + */ + public void processGroupAlert(SingleAlert alert) { + Map<String, String> labels = alert.getLabels(); + if (labels == null || labels.isEmpty() || groupDefines.isEmpty()) { + sendSingleAlert(alert); + return; + } + + // Process each group define rule + boolean matched = false; + for (Map.Entry<String, AlertGroupConverge> define : groupDefines.entrySet()) { + String defineName = define.getKey(); + AlertGroupConverge ruleConfig = define.getValue(); + + // Check if alert has all required group labels + if (hasRequiredLabels(labels, ruleConfig.getGroupLabels())) { + matched = true; + processAlertByGroupDefine(alert, defineName, ruleConfig); + } + } + + if (!matched) { + sendSingleAlert(alert); + } + } + + public void processGroupAlert(Map<String, String> groupLabels, List<SingleAlert> alertList) { + GroupAlert groupAlert = GroupAlert.builder() + .groupKey(generateGroupKey(groupLabels)) + .groupLabels(groupLabels) + .commonLabels(extractCommonLabels(alertList)) + .commonAnnotations(extractCommonAnnotations(alertList)) + .alerts(alertList) + .status(CommonConstants.ALERT_STATUS_FIRING) + .build(); + + alarmInhibitReduce.inhibitAlarm(groupAlert); + } + + private boolean hasRequiredLabels(Map<String, String> labels, List<String> requiredLabels) { + return requiredLabels.stream().allMatch(labels::containsKey); + } + + private void processAlertByGroupDefine(SingleAlert alert, String defineName, AlertGroupConverge ruleConfig) { + // Extract group labels based on define + Map<String, String> extractedLabels = new HashMap<>(); + for (String labelKey : ruleConfig.getGroupLabels()) { + extractedLabels.put(labelKey, alert.getLabels().get(labelKey)); + } + + // Generate group key + String groupKey = generateGroupKey(extractedLabels); + + // Get or create group cache + GroupAlertCache cache = groupCacheMap.computeIfAbsent(groupKey, k -> { + GroupAlertCache newCache = new GroupAlertCache(); + newCache.setGroupKey(groupKey); + newCache.setGroupLabels(extractedLabels); + newCache.setGroupDefineName(defineName); + newCache.setCreateTime(System.currentTimeMillis()); + newCache.setAlertFingerprints(new ConcurrentHashMap<>(8)); + return newCache; + }); + String fingerprint = alert.getFingerprint(); + // Check if this is a duplicate alert + SingleAlert existingAlert = cache.getAlertFingerprints().get(fingerprint); + if (existingAlert != null) { + // Update existing alert timestamp + alert.setStartAt(existingAlert.getStartAt()); + cache.getAlertFingerprints().put(fingerprint, alert); + return; + } + + // Add new alert + cache.getAlertFingerprints().put(fingerprint, alert); + + if (shouldSendGroupImmediately(cache)) { + sendGroupAlert(cache); + cache.setLastSendTime(System.currentTimeMillis()); + cache.getAlertFingerprints().clear(); + } + } + + private void sendGroupAlert(GroupAlertCache cache) { + if (cache.getAlertFingerprints().isEmpty()) { + return; + } + + long now = System.currentTimeMillis(); + String status = determineGroupStatus(cache.getAlertFingerprints().values()); + + // For firing alerts, check repeat interval + if (CommonConstants.ALERT_STATUS_FIRING.equals(status)) { + AlertGroupConverge ruleConfig = groupDefines.get(cache.getGroupDefineName()); + long repeatInterval = ruleConfig.getRepeatInterval() != null + ? ruleConfig.getRepeatInterval() * MS_PER_SECOND : DEFAULT_REPEAT_INTERVAL; + + // Skip if within repeat interval + if (cache.getLastRepeatTime() > 0 + && now - cache.getLastRepeatTime() < repeatInterval) { + return; + } + cache.setLastRepeatTime(now); + } + + GroupAlert groupAlert = GroupAlert.builder() + .groupKey(cache.getGroupKey()) + .groupLabels(cache.getGroupLabels()) + .commonLabels(extractCommonLabels(cache.getAlertFingerprints().values())) + .commonAnnotations(extractCommonAnnotations(cache.getAlertFingerprints().values())) + .alerts(new ArrayList<>(cache.getAlertFingerprints().values())) + .status(status) + .build(); + + alarmInhibitReduce.inhibitAlarm(groupAlert); + } + + private boolean shouldSendGroup(GroupAlertCache cache, long now) { + AlertGroupConverge ruleConfig = groupDefines.get(cache.getGroupDefineName()); + long groupWait = ruleConfig != null ? ruleConfig.getGroupWait() * MS_PER_SECOND : DEFAULT_GROUP_WAIT; + long groupInterval = ruleConfig != null ? ruleConfig.getGroupInterval() * MS_PER_SECOND : DEFAULT_GROUP_INTERVAL; + + // First wait time reached + if (cache.getLastSendTime() == 0 + && now - cache.getCreateTime() >= groupWait) { + return true; + } + // Group interval time reached + return cache.getLastSendTime() > 0 + && now - cache.getLastSendTime() >= groupInterval; + } + + private boolean shouldSendGroupImmediately(GroupAlertCache cache) { + // Check if all alerts are resolved + return cache.getAlertFingerprints().values().stream() + .allMatch(alert -> CommonConstants.ALERT_STATUS_RESOLVED.equals(alert.getStatus())); + } + + private void sendSingleAlert(SingleAlert alert) { + // Wrap single alert as group alert + String groupKey = generateGroupKey(alert.getLabels()); + GroupAlert groupAlert = GroupAlert.builder() + .groupKey(groupKey) + .groupLabels(alert.getLabels()) + .commonLabels(alert.getLabels()) + .commonAnnotations(alert.getAnnotations()) + .alerts(new LinkedList<>(List.of(alert))) + .status(alert.getStatus()) + .build(); + + alarmInhibitReduce.inhibitAlarm(groupAlert); + } + + private String generateGroupKey(Map<String, String> labels) { + return labels.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(e -> e.getKey() + ":" + e.getValue()) + .collect(Collectors.joining(",")); + } + + private Map<String, String> extractCommonLabels(Collection<SingleAlert> alerts) { + // Extract common labels from all alerts + if (alerts.isEmpty()) { + return new HashMap<>(8); + } + Map<String, String> common = new HashMap<>(alerts.stream().findFirst().get().getLabels()); + alerts.forEach(alert -> { + common.keySet().removeIf(key -> + !alert.getLabels().containsKey(key) + || !common.get(key).equals(alert.getLabels().get(key))); + }); + return common; + } + + private Map<String, String> extractCommonAnnotations(Collection<SingleAlert> alerts) { + // Extract common annotations from all alerts + if (alerts.isEmpty()) { + return new HashMap<>(8); + } + Map<String, String> common = new HashMap<>(alerts.stream().findFirst().get().getAnnotations()); + alerts.forEach(alert -> { + common.keySet().removeIf(key -> + !alert.getAnnotations().containsKey(key) + || !common.get(key).equals(alert.getAnnotations().get(key))); + }); + return common; + } + + private String determineGroupStatus(Collection<SingleAlert> alerts) { + // If any alert is firing, group is firing + return alerts.stream() + .anyMatch(alert -> CommonConstants.ALERT_STATUS_FIRING.equals(alert.getStatus())) + ? CommonConstants.ALERT_STATUS_FIRING : CommonConstants.ALERT_STATUS_RESOLVED; + } + + @Data + private static class GroupAlertCache { + private String groupDefineName; + private String groupKey; + private Map<String, String> groupLabels; + private Map<String, SingleAlert> alertFingerprints = new ConcurrentHashMap<>(8); + private long createTime; + private long lastSendTime; + private long lastRepeatTime; + } + + private static final class ScheduledDispatchTask { + + private final ExecutorService executor; + + private final Runnable task; + + private boolean running; + + private int pendingRuns; + + private ScheduledDispatchTask(ExecutorService executor, Runnable task) { + this.executor = executor; + this.task = task; + } + + private void dispatch() { + boolean shouldSchedule; + synchronized (this) { + pendingRuns++; + shouldSchedule = !running; + if (shouldSchedule) { + running = true; + } + } + if (shouldSchedule) { + scheduleRun(); + } + } + + private void scheduleRun() { + if (executor != null) { + executor.execute(this::runOnce); + } else { + runOnce(); + } + } + + private void runOnce() { + try { + task.run(); + } finally { + scheduleNextIfNeeded(); + } + } + + private void scheduleNextIfNeeded() { + boolean shouldSchedule; + synchronized (this) { + pendingRuns = Math.max(0, pendingRuns - 1); + shouldSchedule = pendingRuns > 0; + if (!shouldSchedule) { + running = false; + return; + } + } + scheduleRun(); + } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduce.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduce.java new file mode 100644 index 0000000..4ebfdd2 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduce.java @@ -0,0 +1,440 @@ +/* + * 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 com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.alert.dao.AlertInhibitDao; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertInhibit; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import lombok.Data; +import lombok.AllArgsConstructor; + +/** + * inhibit alarm + * refer from prometheus, code with @cursor + */ +@Component +@Slf4j +public class AlarmInhibitReduce implements DisposableBean { + + /** + * Interval for checking and cleaning up expired source alerts + */ + private static final long CHECK_INTERVAL = 60_000L; + + /** + * alarm silence + */ + private final AlarmSilenceReduce alarmSilenceReduce; + + /** + * rule cache + */ + private final Map<Long, AlertInhibit> inhibitRules; + + /** + * Cache for source alerts + * key: ruleId + * value: map of source alerts with their fingerprints + */ + private final Map<Long, Map<String, SourceAlertEntry>> sourceAlertCache; + + /** + * Default TTL for source alerts (4 hours) + */ + private final long sourceAlertTtl; + + private final ScheduledExecutorService cleanupScheduler; + + private final ExecutorService cleanupExecutor; + + private final ScheduledDispatchTask cleanupTask; + + public AlarmInhibitReduce(AlarmSilenceReduce alarmSilenceReduce, AlertInhibitDao alertInhibitDao + , AlerterProperties alerterProperties) { + this(alarmSilenceReduce, alertInhibitDao, alerterProperties, VirtualThreadProperties.defaults(), true); + } + + @Autowired + public AlarmInhibitReduce(AlarmSilenceReduce alarmSilenceReduce, AlertInhibitDao alertInhibitDao, + AlerterProperties alerterProperties, VirtualThreadProperties virtualThreadProperties) { + this(alarmSilenceReduce, alertInhibitDao, alerterProperties, virtualThreadProperties, true); + } + + AlarmInhibitReduce(AlarmSilenceReduce alarmSilenceReduce, AlertInhibitDao alertInhibitDao, + AlerterProperties alerterProperties, VirtualThreadProperties virtualThreadProperties, + boolean autoStart) { + this.alarmSilenceReduce = alarmSilenceReduce; + VirtualThreadProperties properties = + virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties; + if (alerterProperties.getInhibit() != null && alerterProperties.getInhibit().getTtl() > 0) { + this.sourceAlertTtl = alerterProperties.getInhibit().getTtl(); + } else { + this.sourceAlertTtl = 4 * 60 * 60 * 1000L; + } + inhibitRules = new ConcurrentHashMap<>(8); + sourceAlertCache = new ConcurrentHashMap<>(8); + this.cleanupScheduler = createCleanupScheduler(); + this.cleanupExecutor = createCleanupExecutor(properties); + this.cleanupTask = new ScheduledDispatchTask(cleanupExecutor, this::runCleanupCache); + List<AlertInhibit> inhibits = alertInhibitDao.findAlertInhibitsByEnableIsTrue(); + refreshInhibitRules(inhibits); + if (autoStart) { + startScheduledCleanupCache(); + } + } + + private void startScheduledCleanupCache() { + cleanupScheduler.scheduleAtFixedRate(this::dispatchCleanupCache, CHECK_INTERVAL, CHECK_INTERVAL, + TimeUnit.MILLISECONDS); + } + + void dispatchCleanupCache() { + cleanupTask.dispatch(); + } + + void beforeCleanupCacheRun() { + } + + @Override + public void destroy() { + cleanupScheduler.shutdownNow(); + if (cleanupExecutor != null) { + cleanupExecutor.shutdownNow(); + } + } + + private ScheduledExecutorService createCleanupScheduler() { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("Scheduled clean up inhibit cache has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("inhibit-clean-up-%d") + .build(); + return Executors.newSingleThreadScheduledExecutor(threadFactory); + } + + private ExecutorService createCleanupExecutor(VirtualThreadProperties properties) { + if (!properties.enabled()) { + return null; + } + return Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name("inhibit-clean-up-vt-", 0) + .uncaughtExceptionHandler((thread, throwable) -> { + log.error("Scheduled clean up inhibit cache worker has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .factory()); + } + + private void runCleanupCache() { + beforeCleanupCacheRun(); + try { + sourceAlertCache.values().forEach(this::cleanupExpiredEntries); + sourceAlertCache.entrySet().removeIf(entry -> entry.getValue().isEmpty()); + } catch (Exception e) { + log.error("Error during scheduled cleanup", e); + } + } + + /** + * Configure inhibit rules + * @param rules Inhibit rule list + */ + public void refreshInhibitRules(List<AlertInhibit> rules) { + if (rules == null) { + log.warn("Attempted to refresh inhibit rules with null list."); + return; + } + this.inhibitRules.clear(); + rules.forEach(rule -> this.inhibitRules.put(rule.getId(), rule)); + } + + /** + * Process alert with inhibit rules + * If alert is inhibited, it will not be forwarded + * @param groupAlert Grouped and pending alerts to be processed + */ + public void inhibitAlarm(GroupAlert groupAlert) { + if (groupAlert == null) { + log.warn("Received null GroupAlert. Skipping processing."); + return; + } + try { + if (inhibitRules.isEmpty()) { + alarmSilenceReduce.silenceAlarm(groupAlert); + return; + } + + // Process each individual alert + for (var alert : groupAlert.getAlerts()) { + for (AlertInhibit rule : inhibitRules.values()) { + if (isSourceAlert(alert, rule)) { + cacheSourceAlert(alert, rule); + } + } + } + + // Filter out inhibited alerts + groupAlert.getAlerts().removeIf(this::shouldInhibit); + + // Continue processing if there are remaining alerts + if (!groupAlert.getAlerts().isEmpty()) { + alarmSilenceReduce.silenceAlarm(groupAlert); + } + } catch (Exception e) { + log.error("Error inhibiting alarm for {}", groupAlert, e); + } + } + + /** + * Check if alert matches inhibit rule source labels + * @param alert Single alert to be processed + * @param rule The rule of inhibition + */ + private boolean isSourceAlert(SingleAlert alert, AlertInhibit rule) { + if (alert == null || rule == null) { + log.warn("Received null alert or rule in isSourceAlert"); + return false; + } + if (!CommonConstants.ALERT_STATUS_FIRING.equals(alert.getStatus())) { + return false; + } + return matchLabels(alert.getLabels(), rule.getSourceLabels()); + } + + /** + * Check if alert should be inhibited by any active source alerts + * @param alert Single alert to be processed + */ + private boolean shouldInhibit(SingleAlert alert) { + if (alert == null) { + log.warn("Received null alert in shouldInhibit"); + return false; + } + if (CommonConstants.ALERT_STATUS_RESOLVED.equals(alert.getStatus())) { + return false; + } + + for (AlertInhibit rule : inhibitRules.values()) { + if (!matchLabels(alert.getLabels(), rule.getTargetLabels())) { + continue; + } + + List<SingleAlert> sourceAlerts = getActiveSourceAlerts(rule); + if (sourceAlerts.isEmpty()) { + continue; + } + + for (SingleAlert source : sourceAlerts) { + if (matchEqualLabels(source, alert, rule.getEqualLabels())) { + return true; + } + } + } + return false; + } + + /** + * Check if all required labels match + * @param alertLabels The label of the alarm + * @param requiredLabels Labels to be matched + */ + private boolean matchLabels(Map<String, String> alertLabels, Map<String, String> requiredLabels) { + if (alertLabels == null || requiredLabels == null) { + log.warn("Received null alertLabels or requiredLabels in matchLabels"); + return false; + } + return requiredLabels.entrySet().stream() + .allMatch(entry -> entry.getValue().equals(alertLabels.get(entry.getKey()))); + } + + /** + * Check if equal labels have same values in both alerts + * @param source Alert used to suppress other alerts + * @param target Alert that may be suppressed + * @param equalLabels Need to be equal labels + */ + private boolean matchEqualLabels(SingleAlert source, SingleAlert target, List<String> equalLabels) { + if (source == null || target == null) { + log.warn("Received null source or target in matchEqualLabels"); + return false; + } + if (equalLabels == null || equalLabels.isEmpty()) { + return true; + } + Map<String, String> sourceLabels = source.getLabels(); + Map<String, String> targetLabels = target.getLabels(); + + return equalLabels.stream().allMatch(label -> { + String sourceValue = sourceLabels.get(label); + String targetValue = targetLabels.get(label); + return sourceValue != null && sourceValue.equals(targetValue); + }); + } + + /** + * Cache source alert for inhibit rule + * @param alert Single alert to be processed + * @param rule The rule of inhibition + */ + private void cacheSourceAlert(SingleAlert alert, AlertInhibit rule) { + if (alert == null || rule == null) { + log.warn("Received null alert or rule in cacheSourceAlert"); + return; + } + Map<String, SourceAlertEntry> ruleCache = sourceAlertCache.computeIfAbsent( + rule.getId(), + k -> new ConcurrentHashMap<>() + ); + + SourceAlertEntry entry = new SourceAlertEntry( + alert, + System.currentTimeMillis(), + System.currentTimeMillis() + sourceAlertTtl + ); + ruleCache.put(alert.getFingerprint(), entry); + cleanupExpiredEntries(ruleCache); + } + + /** + * Get active source alerts for inhibit rule + * @param rule The rule of inhibition + * @return List of active source alerts + */ + private List<SingleAlert> getActiveSourceAlerts(AlertInhibit rule) { + if (rule == null) { + log.warn("Received null rule in getActiveSourceAlerts"); + return Collections.emptyList(); + } + Map<String, SourceAlertEntry> ruleCache = sourceAlertCache.get(rule.getId()); + if (ruleCache == null || ruleCache.isEmpty()) { + return Collections.emptyList(); + } + + long now = System.currentTimeMillis(); + return ruleCache.values().stream() + .filter(entry -> entry.getExpiryTime() > now) + .map(SourceAlertEntry::getAlert) + .collect(Collectors.toList()); + } + + /** + * Remove expired entries from cache + * @param cache Source alert cache entry map + */ + private void cleanupExpiredEntries(Map<String, SourceAlertEntry> cache) { + if (cache == null) { + log.warn("Received null cache in cleanupExpiredEntries"); + return; + } + long now = System.currentTimeMillis(); + cache.entrySet().removeIf(entry -> entry.getValue().getExpiryTime() <= now); + } + + /** + * Source alert cache entry + */ + @Data + @AllArgsConstructor + private static class SourceAlertEntry { + private final SingleAlert alert; + private final long createTime; + private final long expiryTime; + } + + private static final class ScheduledDispatchTask { + + private final ExecutorService executor; + + private final Runnable task; + + private boolean running; + + private int pendingRuns; + + private ScheduledDispatchTask(ExecutorService executor, Runnable task) { + this.executor = executor; + this.task = task; + } + + private void dispatch() { + boolean shouldSchedule; + synchronized (this) { + pendingRuns++; + shouldSchedule = !running; + if (shouldSchedule) { + running = true; + } + } + if (shouldSchedule) { + scheduleRun(); + } + } + + private void scheduleRun() { + if (executor != null) { + executor.execute(this::runOnce); + } else { + runOnce(); + } + } + + private void runOnce() { + try { + task.run(); + } finally { + scheduleNextIfNeeded(); + } + } + + private void scheduleNextIfNeeded() { + boolean shouldSchedule; + synchronized (this) { + pendingRuns = Math.max(0, pendingRuns - 1); + shouldSchedule = pendingRuns > 0; + if (!shouldSchedule) { + running = false; + return; + } + } + scheduleRun(); + } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java new file mode 100644 index 0000000..7dfad63 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java @@ -0,0 +1,137 @@ +/* + * 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 java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.hertzbeat.alert.dao.AlertSilenceDao; +import org.apache.hertzbeat.alert.notice.AlertNoticeDispatch; +import org.apache.hertzbeat.common.cache.CacheFactory; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.springframework.stereotype.Component; + +/** + * Silence alert handler + * Refer from prometheus alert silencing mechanism + */ +@Component +@RequiredArgsConstructor +public class AlarmSilenceReduce { + + private final AlertSilenceDao alertSilenceDao; + private final AlertNoticeDispatch dispatcherAlarm; + + /** + * Process alert with silence rules + * If alert matches any active silence rule, it will be silenced + * @param groupAlert The alert to be processed + */ + public void silenceAlarm(GroupAlert groupAlert) { + List<AlertSilence> alertSilenceList = CacheFactory.getAlertSilenceCache(); + if (alertSilenceList == null) { + alertSilenceList = alertSilenceDao.findAlertSilencesByEnableTrue(); + CacheFactory.setAlertSilenceCache(alertSilenceList); + } + + // Check each silence rule + for (AlertSilence alertSilence : alertSilenceList) { + // Check if alert matches silence rule + boolean match = alertSilence.isMatchAll(); + if (!match && groupAlert.getGroupLabels() != null) { + Map<String, String> labels = alertSilence.getLabels(); + Map<String, String> alertLabels = groupAlert.getGroupLabels(); + match = labels.entrySet().stream().anyMatch(item -> + alertLabels.containsKey(item.getKey()) && item.getValue().equals(alertLabels.get(item.getKey()))); + } + + if (match) { + LocalDateTime now = LocalDateTime.now(); + if (alertSilence.getType() == 0) { + // One-time silence rule + if (checkAndSave(now, alertSilence)) { + continue; + } + // Alert is silenced + return; + } else if (alertSilence.getType() == 1) { + // Cyclic silence rule + int currentDayOfWeek = now.getDayOfWeek().getValue(); + if (alertSilence.getDays() != null && alertSilence.getDays().contains((byte) currentDayOfWeek) + && !checkAndSave(now, alertSilence)) { + // Alert is silenced + return; + } + } + } + } + + // No matching silence rule, forward the alert + dispatcherAlarm.dispatchAlarm(groupAlert); + } + + /** + * Check if alert time is within silence period and update silence rule counter + * @param now Current time + * @param alertSilence Silence rule to check + * @return true if alert should not be silenced, false if alert should be silenced + */ + private boolean checkAndSave(LocalDateTime now, AlertSilence alertSilence) { + boolean startMatch; + boolean endMatch; + if (alertSilence.getType() == 1) { + LocalTime nowTime = now.toLocalTime(); + LocalTime startTime = alertSilence.getPeriodStart() == null ? null : alertSilence.getPeriodStart().toLocalTime(); + LocalTime endTime = alertSilence.getPeriodEnd() == null ? null : alertSilence.getPeriodEnd().toLocalTime(); + if (startTime == null && endTime == null) { + startMatch = true; + endMatch = true; + } else if (startTime == null) { + startMatch = true; + endMatch = !nowTime.isAfter(endTime); + } else if (endTime == null) { + startMatch = !nowTime.isBefore(startTime); + endMatch = true; + } else if (!startTime.isAfter(endTime)) { + startMatch = !nowTime.isBefore(startTime); + endMatch = !nowTime.isAfter(endTime); + } else { + // Cross-midnight window, e.g. 23:00-02:00. + startMatch = !nowTime.isBefore(startTime) || !nowTime.isAfter(endTime); + endMatch = true; + } + } else { + startMatch = alertSilence.getPeriodStart() == null + || now.isAfter(alertSilence.getPeriodStart().toLocalDateTime()); + endMatch = alertSilence.getPeriodEnd() == null + || now.isBefore(alertSilence.getPeriodEnd().toLocalDateTime()); + } + + if (startMatch && endMatch) { + int time = Optional.ofNullable(alertSilence.getTimes()).orElse(0); + alertSilence.setTimes(time + 1); + alertSilenceDao.save(alertSilence); + return false; + } + return true; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertDefineImExportService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertDefineImExportService.java new file mode 100644 index 0000000..dfcbb19 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertDefineImExportService.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +/** + * Configuration Import Export + */ +public interface AlertDefineImExportService { + /** + * Import Configuration + * @param is input stream + */ + void importConfig(InputStream is); + + /** + * Export Configuration + * @param os output stream + * @param configList configuration list + */ + void exportConfig(OutputStream os, List<Long> configList); + + /** + * Export file type + * @return file type + */ + String type(); + + /** + * Get Export File Name + * @return file name + */ + String getFileName(); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertDefineService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertDefineService.java new file mode 100644 index 0000000..6b8692a --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertDefineService.java @@ -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.service; + +import jakarta.servlet.http.HttpServletResponse; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.support.exception.AlertExpressionException; +import org.springframework.data.domain.Page; +import org.springframework.web.multipart.MultipartFile; +import org.apache.hertzbeat.common.constants.CommonConstants; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Alarm define manager service + */ +public interface AlertDefineService { + + /** + * Verify the correctness of the request data parameters + * @param alertDefine alertDefine + * @param isModify whether modify + * @throws IllegalArgumentException A checksum parameter error is thrown + */ + void validate(AlertDefine alertDefine, boolean isModify) throws IllegalArgumentException; + + /** + * New Alarm Definition + * @param alertDefine Alarm definition Entity + * @throws RuntimeException Added procedure exception throwing + */ + void addAlertDefine(AlertDefine alertDefine) throws RuntimeException; + + /** + * Modifying an Alarm Definition + * @param alertDefine Alarm definition Entity + * @throws RuntimeException Exception thrown during modification + */ + void modifyAlertDefine(AlertDefine alertDefine) throws RuntimeException; + + /** + * Deleting an Alarm Definition + * @param alertId Alarm Definition ID + * @throws RuntimeException Exception thrown during deletion + */ + void deleteAlertDefine(long alertId) throws RuntimeException; + + /** + * Obtain alarm definition information + * @param alertId Monitor the ID + * @return AlertDefine + * @throws RuntimeException An exception was thrown during the query + */ + AlertDefine getAlertDefine(long alertId) throws RuntimeException; + + + /** + * Delete alarm definitions in batches + * @param alertIds Alarm Definition IDs + * @throws RuntimeException Exception thrown during deletion + */ + void deleteAlertDefines(Set<Long> alertIds) throws RuntimeException; + + /** + * Dynamic conditional query + * @param defineIds Alarm Definition ID List + * @param search Search-Target Expr Template + * @param sort Sort field + * @param order Sort mode: asc: ascending, desc: descending + * @param pageIndex List current page + * @param pageSize Number of list pages + * @return The query results + */ + Page<AlertDefine> getAlertDefines(List<Long> defineIds, String search, String sort, String order, int pageIndex, int pageSize); + + /** + * Export file configuration of specified type based on ID list and export file type + * @param ids AlertDefine ID + * @param type File Type + * @param res Response + * @throws Exception An exception was thrown during the export + */ + void export(List<Long> ids, String type, HttpServletResponse res) throws Exception; + + /** + * Add alarm threshold rules based on the uploaded alarm threshold file + * @param file Upload File + * @throws Exception An exception was thrown during the importConfig + */ + void importConfig(MultipartFile file) throws Exception; + + /** + * Get the real-time metrics alarm definition list + * @return Real-time metrics alarm definition list + */ + List<AlertDefine> getMetricsRealTimeAlertDefines(); + + /** + * Get the real-time log alarm definition list + * @return Real-time log alarm definition list + */ + List<AlertDefine> getLogRealTimeAlertDefines(); + + /** + * Get the alarm definition list by type + * @param type Alarm definition type, must be one of the constants defined in {@link CommonConstants} + * @return List of enabled alarm definitions matching the specified type, empty list if none found + */ + List<AlertDefine> getAlertDefinesByType(String type); + + /** + * Get define preview + * @return Data queried based on expressions + * @throws AlertExpressionException expression error + */ + List<Map<String, Object>> getDefinePreview(String datasource, String type, String expr); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertGroupConvergeService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertGroupConvergeService.java new file mode 100644 index 0000000..6ae319b --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertGroupConvergeService.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service; + +import java.util.List; +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.springframework.data.domain.Page; + +/** + * management interface service for alert converge + */ +public interface AlertGroupConvergeService { + /** + * Verify the correctness of the request data parameters + * @param alertGroupConverge AlertGroupConverge + * @param isModify whether modify + * @throws IllegalArgumentException A checksum parameter error is thrown + */ + void validate(AlertGroupConverge alertGroupConverge, boolean isModify) throws IllegalArgumentException; + + /** + * New AlertGroupConverge + * @param alertGroupConverge AlertGroupConverge Entity + * @throws RuntimeException Added procedure exception throwing + */ + void addAlertGroupConverge(AlertGroupConverge alertGroupConverge) throws RuntimeException; + + /** + * Modifying an AlertGroupConverge + * @param alertGroupConverge Alarm definition Entity + * @throws RuntimeException Exception thrown during modification + */ + void modifyAlertGroupConverge(AlertGroupConverge alertGroupConverge) throws RuntimeException; + + /** + * Obtain AlertGroupConverge information + * @param convergeId AlertGroupConverge ID + * @return AlertGroupConverge + * @throws RuntimeException An exception was thrown during the query + */ + AlertGroupConverge getAlertGroupConverge(long convergeId) throws RuntimeException; + + + /** + * Delete AlertGroupConverge in batches + * @param convergeIds AlertGroupConverge IDs + * @throws RuntimeException Exception thrown during deletion + */ + void deleteAlertGroupConverges(Set<Long> convergeIds) throws RuntimeException; + + /** + * Dynamic conditional query + * @param convergeIds Alarm Converge ID List + * @param search Search Name + * @param sort Sort field + * @param order Sort mode: asc: ascending, desc: descending + * @param pageIndex List current page + * @param pageSize Number of list pages + * @return The query results + */ + Page<AlertGroupConverge> getAlertGroupConverges(List<Long> convergeIds, String search, String sort, String order, int pageIndex, int pageSize); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertInhibitService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertInhibitService.java new file mode 100644 index 0000000..4354893 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertInhibitService.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service; + +import java.util.List; +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertInhibit; +import org.springframework.data.domain.Page; + +/** + * management interface service for alert inhibit + */ +public interface AlertInhibitService { + /** + * Verify the correctness of the request data parameters + * @param alertInhibit AlertInhibit + * @param isModify whether modify + * @throws IllegalArgumentException A checksum parameter error is thrown + */ + void validate(AlertInhibit alertInhibit, boolean isModify) throws IllegalArgumentException; + + /** + * New AlertInhibit + * @param alertInhibit AlertInhibit Entity + * @throws RuntimeException Added procedure exception throwing + */ + void addAlertInhibit(AlertInhibit alertInhibit) throws RuntimeException; + + /** + * Modifying an AlertInhibit + * @param alertInhibit Alarm definition Entity + * @throws RuntimeException Exception thrown during modification + */ + void modifyAlertInhibit(AlertInhibit alertInhibit) throws RuntimeException; + + /** + * Obtain AlertInhibit information + * @param inhibitId AlertInhibit ID + * @return AlertInhibit + * @throws RuntimeException An exception was thrown during the query + */ + AlertInhibit getAlertInhibit(long inhibitId) throws RuntimeException; + + + /** + * Delete AlertInhibit in batches + * @param inhibitIds AlertInhibit IDs + * @throws RuntimeException Exception thrown during deletion + */ + void deleteAlertInhibits(Set<Long> inhibitIds) throws RuntimeException; + + /** + * Dynamic conditional query + * @param inhibitIds Alarm Silence ID + * @param search Search Name + * @param sort Sort field + * @param order Sort mode: asc: ascending, desc: descending + * @param pageIndex List current page + * @param pageSize Number of list pages + * @return The query results + */ + Page<AlertInhibit> getAlertInhibits(List<Long> inhibitIds, String search, String sort, String order, int pageIndex, int pageSize); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java new file mode 100644 index 0000000..ea5c9bb --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java @@ -0,0 +1,86 @@ +/* + * 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.service; + +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.dto.AlertSummary; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.springframework.data.domain.Page; + +/** + * Alarm information management interface + */ +public interface AlertService { + + /** + * get and query single alerts + * @param status status + * @param search search + * @param sort sort + * @param order order + * @param pageIndex pageIndex + * @param pageSize pageSize + * @return single alerts + */ + Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize); + + /** + * Dynamic conditional query + * @param status Alarm Status + * @param sort Sort field + * @param order Sort Type + * @param pageIndex List current page + * @param pageSize Number of list pagination + * @return search result + */ + Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize); + + /** + * delete the group alarm according to the alarm ID + * @param ids Alarm ID List + */ + void deleteGroupAlerts(HashSet<Long> ids); + + /** + * delete the single alarm according to the alarm ID + * @param ids Alarm ID List + */ + void deleteSingleAlerts(HashSet<Long> ids); + + /** + * Update the alarm status according to the alarm ID-status value + * @param status Alarm status to be modified + * @param ids Alarm ID List to be modified + */ + void editGroupAlertStatus(String status, List<Long> ids); + + /** + * Update the alarm status according to the alarm ID-status value + * @param status Alarm status to be modified + * @param ids Alarm ID List to be modified + */ + void editSingleAlertStatus(String status, List<Long> ids); + + /** + * Get alarm statistics information + * @return Alarm statistics information + */ + AlertSummary getAlertsSummary(); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertSilenceService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertSilenceService.java new file mode 100644 index 0000000..36846b6 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertSilenceService.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service; + +import java.util.List; +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.springframework.data.domain.Page; + +/** + * management interface service for alert silence + */ +public interface AlertSilenceService { + /** + * Verify the correctness of the request data parameters + * @param alertSilence AlertSilence + * @param isModify whether modify + * @throws IllegalArgumentException A checksum parameter error is thrown + */ + void validate(AlertSilence alertSilence, boolean isModify) throws IllegalArgumentException; + + /** + * New AlertSilence + * @param alertSilence AlertSilence Entity + * @throws RuntimeException Added procedure exception throwing + */ + void addAlertSilence(AlertSilence alertSilence) throws RuntimeException; + + /** + * Modifying an AlertSilence + * @param alertSilence Alarm definition Entity + * @throws RuntimeException Exception thrown during modification + */ + void modifyAlertSilence(AlertSilence alertSilence) throws RuntimeException; + + /** + * Obtain AlertSilence information + * @param silenceId AlertSilence ID + * @return AlertSilence + * @throws RuntimeException An exception was thrown during the query + */ + AlertSilence getAlertSilence(long silenceId) throws RuntimeException; + + + /** + * Delete AlertSilence in batches + * @param silenceIds AlertSilence IDs + * @throws RuntimeException Exception thrown during deletion + */ + void deleteAlertSilences(Set<Long> silenceIds) throws RuntimeException; + + /** + * Dynamic conditional query + * @param silenceIds Alarm Silence ID + * @param search Search Name + * @param sort Sort field + * @param order Sort mode: asc: ascending, desc: descending + * @param pageIndex List current page + * @param pageSize Number of list pages + * @return The query results + */ + Page<AlertSilence> getAlertSilences(List<Long> silenceIds, String search, String sort, String order, int pageIndex, int pageSize); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/DataSourceService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/DataSourceService.java new file mode 100644 index 0000000..a3dc79b --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/DataSourceService.java @@ -0,0 +1,49 @@ +/* + * 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.service; + +import java.util.List; +import java.util.Map; + +/** + * datasource service + */ +public interface DataSourceService { + + /** + * execute query expr calculate + * @param datasource datasource + * @param expr query expr + * @return result + */ + List<Map<String, Object>> calculate(String datasource, String expr); + + /** + * query result set from db + * @param datasource sql or promql + * @param expr query expr + * @return result + */ + List<Map<String, Object>> query(String datasource, String expr); + + /** + * Get available datasource executors status + * @return map containing available executors by type (promql, sql) + */ + Map<String, Object> getAvailableExecutors(); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/ExternAlertService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/ExternAlertService.java new file mode 100644 index 0000000..346b0b2 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/ExternAlertService.java @@ -0,0 +1,37 @@ +/* + * 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.service; + +/** + * Extern alert service for prometheus, cloud and so on + */ +public interface ExternAlertService { + + /** + * Save external alarms of services + * @param content alert report json string + */ + void addExternAlert(String content); + + + /** + * Support source: prometheus, tencent ... + * @return source name + */ + String supportSource(); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/NoticeConfigService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/NoticeConfigService.java new file mode 100644 index 0000000..8a318db --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/NoticeConfigService.java @@ -0,0 +1,178 @@ +/* + * 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.service; + +import java.util.List; +import java.util.Optional; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +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.springframework.data.domain.Page; + +/** + * Message notification configuration interface + */ +public interface NoticeConfigService { + + /** + * Dynamic conditional query + * @param name Recipient name,support fuzzy query + * @param pageIndex Page number + * @param pageSize Number of records per page + * @return Search result + */ + Page<NoticeReceiver> getNoticeReceivers(String name, int pageIndex, int pageSize); + + /** + * Dynamic conditional query + * @param name Template name,support fuzzy query + * @param preset Whether it is a system preset template + * true: System preset template + * false: Custom template + * @param pageIndex Page number + * @param pageSize Number of records per page + * @return Search result + */ + Page<NoticeTemplate> getNoticeTemplates(String name, boolean preset, int pageIndex, int pageSize); + + /** + * Dynamic conditional query + * @param name Recipient name ,support fuzzy query + * @param pageIndex Page number + * @param pageSize Number of records per page + * @return Search result + */ + Page<NoticeRule> getNoticeRules(String name, int pageIndex, int pageSize); + + /** + * Add a notification recipient + * @param noticeReceiver recipient information + */ + void addReceiver(NoticeReceiver noticeReceiver); + + /** + * Modify notification recipients + * @param noticeReceiver recipient information + */ + void editReceiver(NoticeReceiver noticeReceiver); + + /** + * Delete recipient information based on recipient ID + * @param receiverId Recipient ID + */ + void deleteReceiver(Long receiverId); + + /** + * Added notification policy + * @param noticeRule Notification strategy + */ + void addNoticeRule(NoticeRule noticeRule); + + /** + * Modify Notification Policy + * @param noticeRule Notification strategy + */ + void editNoticeRule(NoticeRule noticeRule); + + /** + * Delete the specified notification policy + * @param ruleId Notification Policy ID + */ + void deleteNoticeRule(Long ruleId); + + /** + * According to the alarm information matching all notification policies, filter out the recipients who need to be notified + * @param alert Alarm information + * @return Receiver + */ + List<NoticeRule> getReceiverFilterRule(GroupAlert alert); + + /** + * Query the template information according to the template ID + * @param id Template ID + * @return Template + */ + NoticeTemplate getOneTemplateById(Long id); + + /** + * Query recipient information based on recipient ID (primary key Id) + * @param receiverId Receiver ID (primary key ID) + * @return Recipient Entity + */ + NoticeReceiver getReceiverById(Long receiverId); + + /** + * Query specific notification rules according to the rule ID (primary key ID) + * @param ruleId Rule ID + * @return Notification Rule Entity + */ + NoticeRule getNoticeRulesById(Long ruleId); + + + /** + * Add a notification template + * @param noticeTemplate template information + */ + void addNoticeTemplate(NoticeTemplate noticeTemplate); + + /** + * Modify notification templates + * @param noticeTemplate template information + */ + void editNoticeTemplate(NoticeTemplate noticeTemplate); + + /** + * Delete template information based on Template ID + * @param templateId Template ID + */ + void deleteNoticeTemplate(Long templateId); + + /** + * Query specific notification templates according to the template ID (primary key ID) + * @param templateId Template ID + * @return Notification Template Entity + */ + Optional<NoticeTemplate> getNoticeTemplatesById(Long templateId); + + /** + * Query specific notification templates according to the template type + * @param type Template type + * @return Notification Template Entity + */ + NoticeTemplate getDefaultNoticeTemplateByType(Byte type); + + /** + * alert Send test message + * @param noticeReceiver recipient information + * @return true send success | false send fail + */ + boolean sendTestMsg(NoticeReceiver noticeReceiver); + + /** + * Query all notification recipients + * @return Recipient List + */ + List<NoticeReceiver> getAllNoticeReceivers(); + + /** + * Query all notification policies + * @return Notification Policy List + */ + List<NoticeTemplate> getAllNoticeTemplates(); +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/SmsClient.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/SmsClient.java new file mode 100644 index 0000000..2b159f3 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/SmsClient.java @@ -0,0 +1,42 @@ +/* + * 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.service; + +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; + +/** + * SMS client interface + */ +public interface SmsClient { + /** + * send SMS + */ + void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert); + + /** + * get SMS provider type + */ + String getType(); + + /** + * check SMS configuration, return true if the configuration is correct + */ + boolean checkConfig(); +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/SmsClientFactory.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/SmsClientFactory.java new file mode 100644 index 0000000..0047d0f --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/SmsClientFactory.java @@ -0,0 +1,152 @@ +/* + * 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.service; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig; +import org.apache.hertzbeat.alert.service.impl.SmsLocalSmsClientImpl; +import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl; +import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl; +import org.apache.hertzbeat.alert.service.impl.TwilioSmsClientImpl; +import org.apache.hertzbeat.alert.service.impl.UniSmsClientImpl; +import org.apache.hertzbeat.alert.service.impl.AlibabaSmsClientImpl; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum; +import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.apache.hertzbeat.common.support.event.SmsConfigChangeEvent; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import static org.apache.hertzbeat.common.constants.SmsConstants.ALIBABA; +import static org.apache.hertzbeat.common.constants.SmsConstants.AWS; +import static org.apache.hertzbeat.common.constants.SmsConstants.TENCENT; +import static org.apache.hertzbeat.common.constants.SmsConstants.TWILIO; +import static org.apache.hertzbeat.common.constants.SmsConstants.UNISMS; +import static org.apache.hertzbeat.common.constants.SmsConstants.SMSLOCAL; + +/** + * SMS client factory + */ +@Slf4j +@Component +public class SmsClientFactory { + + private static final String TYPE = GeneralConfigTypeEnum.sms.name(); + + private final GeneralConfigDao generalConfigDao; + private final SmsConfig yamlSmsConfig; + + private volatile SmsClient currentSmsClient; + + public SmsClientFactory(GeneralConfigDao generalConfigDao, SmsConfig yamlSmsConfig) { + this.generalConfigDao = generalConfigDao; + this.yamlSmsConfig = yamlSmsConfig; + } + + /** + * SMS configuration change event listener + */ + @EventListener(SmsConfigChangeEvent.class) + public void onSmsConfigChange(SmsConfigChangeEvent event) { + log.info("[SmsClientFactory] SMS configuration change event received"); + synchronized (this) { + currentSmsClient = null; + } + } + + public SmsClient getSmsClient() { + if (currentSmsClient != null) { + return currentSmsClient; + } + synchronized (this) { + if (currentSmsClient != null) { + return currentSmsClient; + } + loadConfig(); + return currentSmsClient; + } + } + + private void loadConfig() { + try { + // 1. try to load database configuration + SmsConfig dbConfig = loadDatabaseConfig(); + if (dbConfig != null && !dbConfig.getType().isBlank() && dbConfig.isEnable()) { + createSmsClient(dbConfig); + if (currentSmsClient != null) { + log.info("[SmsClientFactory] Using database SMS configuration, provider: {}", dbConfig.getType()); + return; + } + } + + // 2. try to load YAML configuration + if (yamlSmsConfig != null && !yamlSmsConfig.getType().isBlank() && yamlSmsConfig.isEnable()) { + createSmsClient(yamlSmsConfig); + if (currentSmsClient != null) { + log.info("[SmsClientFactory] Using YAML SMS configuration, provider: {}", yamlSmsConfig.getType()); + return; + } + } + + log.warn("[SmsClientFactory] No valid SMS configuration found"); + + } catch (Exception e) { + log.error("[SmsClientFactory] Failed to load SMS configuration", e); + currentSmsClient = null; + } + } + + private SmsConfig loadDatabaseConfig() { + try { + GeneralConfig config = generalConfigDao.findByType(TYPE); + if (config != null && config.getContent() != null) { + return JsonUtil.fromJson(config.getContent(), SmsConfig.class); + } + } catch (Exception e) { + log.error("[SmsClientFactory] Failed to load database configuration", e); + } + return null; + } + + private void createSmsClient(SmsConfig smsConfig) { + switch (smsConfig.getType()) { + case TENCENT: + currentSmsClient = new TencentSmsClientImpl(smsConfig.getTencent()); + break; + case UNISMS: + currentSmsClient = new UniSmsClientImpl(smsConfig.getUnisms()); + break; + case ALIBABA: + currentSmsClient = new AlibabaSmsClientImpl(smsConfig.getAlibaba()); + break; + case SMSLOCAL: + currentSmsClient = new SmsLocalSmsClientImpl(smsConfig.getSmslocal()); + break; + case AWS: + currentSmsClient = new AwsSmsClientImpl(smsConfig.getAws()); + break; + case TWILIO: + currentSmsClient = new TwilioSmsClientImpl(smsConfig.getTwilio()); + break; + default: + log.warn("[SmsClientFactory] Unsupported SMS provider type: {}", smsConfig.getType()); + break; + } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineAbstractImExportServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineAbstractImExportServiceImpl.java new file mode 100644 index 0000000..de7c85e --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineAbstractImExportServiceImpl.java @@ -0,0 +1,109 @@ +/* + * 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.service.impl; + +import jakarta.annotation.Resource; +import java.io.InputStream; +import java.io.OutputStream; +import java.time.LocalDate; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dto.AlertDefineDTO; +import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO; +import org.apache.hertzbeat.alert.service.AlertDefineImExportService; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.util.LogUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.util.CollectionUtils; + +/** + * Configuration Import Export + */ +@Slf4j +public abstract class AlertDefineAbstractImExportServiceImpl implements AlertDefineImExportService { + @Resource + @Lazy + private AlertDefineService alertDefineService; + + private static final Logger logger = LoggerFactory.getLogger(AlertDefineAbstractImExportServiceImpl.class); + + @Override + public void importConfig(InputStream is) { + var formList = parseImport(is) + .stream() + .map(this::convert) + .toList(); + LogUtil.info(logger, "Importing alert defines from {0}", formList); + if (!CollectionUtils.isEmpty(formList)) { + formList.forEach(alertDefine -> { + alertDefineService.validate(alertDefine, false); + alertDefineService.addAlertDefine(alertDefine); + }); + } + } + + @Override + public void exportConfig(OutputStream os, List<Long> configList) { + var monitorList = configList.stream() + .map(it -> alertDefineService.getAlertDefine(it)) + .map(this::convert) + .toList(); + writeOs(monitorList, os); + } + + + /** + * Parsing an input stream into a form + * + * @param is input stream + * @return form list + */ + abstract List<ExportAlertDefineDTO> parseImport(InputStream is); + + /** + * Export Configuration to Output Stream + * @param exportAlertDefineList configuration list + * @param os output stream + */ + abstract void writeOs(List<ExportAlertDefineDTO> exportAlertDefineList, OutputStream os); + + + private ExportAlertDefineDTO convert(AlertDefine alertDefine) { + var exportAlertDefine = new ExportAlertDefineDTO(); + var alertDefineDTO = new AlertDefineDTO(); + BeanUtils.copyProperties(alertDefine, alertDefineDTO); + exportAlertDefine.setAlertDefine(alertDefineDTO); + return exportAlertDefine; + } + + private AlertDefine convert(ExportAlertDefineDTO exportAlertDefineDTO) { + var alertDefine = new AlertDefine(); + var alertDefineDTO = exportAlertDefineDTO.getAlertDefine(); + BeanUtils.copyProperties(alertDefineDTO, alertDefine); + return alertDefine; + } + + protected String fileNamePrefix() { + return "hertzbeat_alertDefine_" + LocalDate.now(); + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineExcelImExportServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineExcelImExportServiceImpl.java new file mode 100644 index 0000000..91feab3 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineExcelImExportServiceImpl.java @@ -0,0 +1,237 @@ +/* + * 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.service.impl; + +import static org.apache.hertzbeat.common.constants.ExportFileConstants.ExcelFile.FILE_SUFFIX; +import static org.apache.hertzbeat.common.constants.ExportFileConstants.ExcelFile.TYPE; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dto.AlertDefineDTO; +import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import tools.jackson.core.type.TypeReference; + +/** + * Configure the import and export EXCEL format + */ +@Slf4j +@Service +public class AlertDefineExcelImExportServiceImpl extends AlertDefineAbstractImExportServiceImpl { + + /** + * Export file type + * @return file type + */ + @Override + public String type() { + return TYPE; + } + + /** + * Get Export File Name + * @return file name + */ + @Override + public String getFileName() { + return fileNamePrefix() + FILE_SUFFIX; + } + + + /** + * Parsing an input stream into a form + * @param is input stream + * @return form list + */ + @Override + public List<ExportAlertDefineDTO> parseImport(InputStream is) { + try (Workbook workbook = WorkbookFactory.create(is)) { + Sheet sheet = workbook.getSheetAt(0); + List<ExportAlertDefineDTO> alertDefines = new ArrayList<>(); + for (Row row : sheet) { + if (row.getRowNum() == 0) { + continue; + } + String app = getCellValueAsString(row.getCell(0)); + if (StringUtils.hasText(app)) { + AlertDefineDTO alertDefineDTO = extractAlertDefineDataFromRow(row); + ExportAlertDefineDTO exportAlertDefineDTO = new ExportAlertDefineDTO(); + exportAlertDefineDTO.setAlertDefine(alertDefineDTO); + alertDefines.add(exportAlertDefineDTO); + } + } + return alertDefines; + } catch (IOException e) { + throw new RuntimeException("Failed to parse alertDefine data", e); + } + } + + + private String getCellValueAsString(Cell cell) { + if (cell == null) { + return null; + } + return switch (cell.getCellType()) { + case STRING -> cell.getStringCellValue(); + case NUMERIC -> String.valueOf(cell.getNumericCellValue()); + default -> null; + }; + } + + + private boolean getCellValueAsBoolean(Cell cell) { + if (cell == null) { + return false; + } + if (Objects.requireNonNull(cell.getCellType()) == CellType.BOOLEAN) { + return cell.getBooleanCellValue(); + } + return false; + } + + + private Integer getCellValueAsInteger(Cell cell) { + if (cell == null) { + return null; + } + if (Objects.requireNonNull(cell.getCellType()) == CellType.NUMERIC) { + return (int) cell.getNumericCellValue(); + } + return null; + } + + + private Byte getCellValueAsByte(Cell cell) { + if (cell == null) { + return null; + } + if (Objects.requireNonNull(cell.getCellType()) == CellType.NUMERIC) { + return (byte) cell.getNumericCellValue(); + } + return null; + } + + + private AlertDefineDTO extractAlertDefineDataFromRow(Row row) { + TypeReference<Map<String, String>> typeReference = new TypeReference<>() {}; + AlertDefineDTO alertDefineDTO = new AlertDefineDTO(); + alertDefineDTO.setName(getCellValueAsString(row.getCell(0))); + alertDefineDTO.setType(getCellValueAsString(row.getCell(1))); + alertDefineDTO.setExpr(getCellValueAsString(row.getCell(2))); + alertDefineDTO.setPeriod(getCellValueAsInteger(row.getCell(3))); + alertDefineDTO.setTimes(getCellValueAsInteger(row.getCell(4))); + alertDefineDTO.setLabels(JsonUtil.fromJson(getCellValueAsString(row.getCell(5)), typeReference)); + alertDefineDTO.setAnnotations(JsonUtil.fromJson(getCellValueAsString(row.getCell(6)), typeReference)); + alertDefineDTO.setTemplate(getCellValueAsString(row.getCell(7))); + alertDefineDTO.setEnable(getCellValueAsBoolean(row.getCell(8))); + return alertDefineDTO; + } + + + /** + * Export Configuration to Output Stream + * @param exportAlertDefineList exportAlertDefineList + * @param os output stream + */ + @Override + public void writeOs(List<ExportAlertDefineDTO> exportAlertDefineList, OutputStream os) { + try { + + Workbook workbook = WorkbookFactory.create(true); + String sheetName = "Export AlertDefine"; + Sheet sheet = workbook.createSheet(sheetName); + sheet.setDefaultColumnWidth(20); + sheet.setColumnWidth(2, 40 * 256); + sheet.setColumnWidth(5, 40 * 256); + sheet.setColumnWidth(6, 40 * 256); + sheet.setColumnWidth(7, 40 * 256); + // set header style + CellStyle headerCellStyle = workbook.createCellStyle(); + Font headerFont = workbook.createFont(); + headerFont.setBold(true); + headerCellStyle.setFont(headerFont); + headerCellStyle.setAlignment(HorizontalAlignment.CENTER); + // set cell style + CellStyle cellStyle = workbook.createCellStyle(); + cellStyle.setAlignment(HorizontalAlignment.CENTER); + // set header + String[] headers = {"Name", "Type", "Expr", "Period", "Times", "Labels", "Annotations", "Template", "Enable"}; + Row headerRow = sheet.createRow(0); + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + cell.setCellStyle(headerCellStyle); + } + + // Traverse the threshold rule list, each threshold rule object corresponds to a row of data + int rowIndex = 1; + for (ExportAlertDefineDTO alertDefine : exportAlertDefineList) { + AlertDefineDTO alertDefineDTO = alertDefine.getAlertDefine(); + Row row = sheet.createRow(rowIndex++); + // Threshold rule information only needs to be written once + Cell nameCell = row.createCell(0); + nameCell.setCellValue(alertDefineDTO.getName()); + nameCell.setCellStyle(cellStyle); + Cell typeCell = row.createCell(1); + typeCell.setCellValue(alertDefineDTO.getType()); + typeCell.setCellStyle(cellStyle); + Cell exprCell = row.createCell(2); + exprCell.setCellValue(alertDefineDTO.getExpr()); + exprCell.setCellStyle(cellStyle); + Cell periodCell = row.createCell(3); + periodCell.setCellValue(alertDefineDTO.getPeriod()); + periodCell.setCellStyle(cellStyle); + Cell timesCell = row.createCell(4); + timesCell.setCellValue(alertDefineDTO.getTimes()); + timesCell.setCellStyle(cellStyle); + Cell labelsCell = row.createCell(5); + labelsCell.setCellValue(JsonUtil.toJson(alertDefineDTO.getLabels())); + labelsCell.setCellStyle(cellStyle); + Cell annoCell = row.createCell(6); + annoCell.setCellValue(JsonUtil.toJson(alertDefineDTO.getAnnotations())); + annoCell.setCellStyle(cellStyle); + Cell templateCell = row.createCell(7); + templateCell.setCellValue(alertDefineDTO.getTemplate()); + templateCell.setCellStyle(cellStyle); + Cell enableCell = row.createCell(8); + enableCell.setCellValue(alertDefineDTO.getEnable()); + enableCell.setCellStyle(cellStyle); + } + workbook.write(os); + os.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineJsonImExportServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineJsonImExportServiceImpl.java new file mode 100644 index 0000000..1379c5e --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineJsonImExportServiceImpl.java @@ -0,0 +1,58 @@ +/* + * 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.service.impl; + +import static org.apache.hertzbeat.common.constants.ExportFileConstants.JsonFile.FILE_SUFFIX; +import static org.apache.hertzbeat.common.constants.ExportFileConstants.JsonFile.TYPE; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.stereotype.Service; +import tools.jackson.core.type.TypeReference; + +/** + * Configure the import and export JSON format + */ +@Slf4j +@Service +public class AlertDefineJsonImExportServiceImpl extends AlertDefineAbstractImExportServiceImpl { + + @Override + public String type() { + return TYPE; + } + + @Override + public String getFileName() { + return fileNamePrefix() + FILE_SUFFIX; + } + + @Override + public List<ExportAlertDefineDTO> parseImport(InputStream is) { + return JsonUtil.fromJson(is, new TypeReference<>() { + }); + } + + @Override + public void writeOs(List<ExportAlertDefineDTO> exportAlertDefineList, OutputStream os) { + JsonUtil.toJson(exportAlertDefineList, os); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java new file mode 100644 index 0000000..f08234a --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java @@ -0,0 +1,309 @@ +/* + * 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.service.impl; + +import jakarta.annotation.Resource; +import tools.jackson.core.type.TypeReference; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler; +import org.apache.hertzbeat.alert.dao.AlertDefineDao; +import org.apache.hertzbeat.alert.service.AlertDefineImExportService; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.alert.service.DataSourceService; +import org.apache.hertzbeat.base.dao.LabelDao; +import org.apache.hertzbeat.base.service.LabelService; +import org.apache.hertzbeat.common.cache.CacheFactory; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.constants.ExportFileConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.manager.Label; +import org.apache.hertzbeat.common.util.FileUtil; +import org.apache.hertzbeat.common.util.JexlExpressionRunner; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Alarm definition management interface implementation + */ +@Service +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class AlertDefineServiceImpl implements AlertDefineService { + + @Autowired + private AlertDefineDao alertDefineDao; + + @Autowired + private PeriodicAlertRuleScheduler periodicAlertRuleScheduler; + + @Resource + private LabelService labelService; + + @Resource + private LabelDao labelDao; + + private final DataSourceService dataSourceService; + + private final Map<String, AlertDefineImExportService> alertDefineImExportServiceMap = new HashMap<>(); + + private static final String CONTENT_TYPE = MediaType.APPLICATION_OCTET_STREAM_VALUE + SignConstants.SINGLE_MARK + "charset=" + StandardCharsets.UTF_8; + + private static final Set<String> SYSTEM_BUILT_IN_LABELS = Set.of( + CommonConstants.LABEL_INSTANCE, + CommonConstants.LABEL_DEFINE_ID, + CommonConstants.LABEL_ALERT_NAME, + CommonConstants.LABEL_INSTANCE_NAME, + CommonConstants.LABEL_ALERT_SEVERITY, + CommonConstants.ALERT_MODE_LABEL + ); + + public AlertDefineServiceImpl(List<AlertDefineImExportService> alertDefineImExportServiceList, DataSourceService dataSourceService) { + alertDefineImExportServiceList.forEach(it -> alertDefineImExportServiceMap.put(it.type(), it)); + this.dataSourceService = dataSourceService; + } + + @Override + public void validate(AlertDefine alertDefine, boolean isModify) throws IllegalArgumentException { + if (StringUtils.hasText(alertDefine.getExpr())) { + if (CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType()) + || CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())) { + try { + JexlExpressionRunner.compile(alertDefine.getExpr()); + } catch (Exception e) { + throw new IllegalArgumentException("alert expr error: " + e.getMessage()); + } + } + } + // the name of the alarm rule is unique + Optional<AlertDefine> optional = alertDefineDao.findAlertDefineByName(alertDefine.getName()); + if (optional.isPresent()) { + if (!isModify || !optional.get().getId().equals(alertDefine.getId())) { + throw new IllegalArgumentException("alert name already exists"); + } + } + } + + @Override + public void addAlertDefine(AlertDefine alertDefine) throws RuntimeException { + saveNewCustomLabel(alertDefine); + alertDefine = alertDefineDao.saveAndFlush(alertDefine); + periodicAlertRuleScheduler.updateSchedule(alertDefine); + CacheFactory.clearAlertDefineCache(); + } + + @Override + public void modifyAlertDefine(AlertDefine alertDefine) throws RuntimeException { + saveNewCustomLabel(alertDefine); + alertDefineDao.saveAndFlush(alertDefine); + periodicAlertRuleScheduler.updateSchedule(alertDefine); + CacheFactory.clearAlertDefineCache(); + } + + private void saveNewCustomLabel(AlertDefine alertDefine) { + Map<String, String> labels = alertDefine.getLabels(); + if (labels == null) { + labels = new HashMap<>(8); + alertDefine.setLabels(labels); + } + Map<String, String> customLabels = labels.entrySet().stream() + .filter(entry -> !isSystemBuiltInLabel(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + List<Label> addLabels = labelService.determineNewLabels(customLabels.entrySet()); + if (!addLabels.isEmpty()) { + labelDao.saveAll(addLabels); + } + } + + private boolean isSystemBuiltInLabel(String labelKey) { + return SYSTEM_BUILT_IN_LABELS.contains(labelKey); + } + + @Override + public void deleteAlertDefine(long alertId) throws RuntimeException { + alertDefineDao.deleteById(alertId); + periodicAlertRuleScheduler.cancelSchedule(alertId); + CacheFactory.clearAlertDefineCache(); + } + + @Override + public AlertDefine getAlertDefine(long alertId) throws RuntimeException { + Optional<AlertDefine> optional = alertDefineDao.findById(alertId); + return optional.orElse(null); + } + + @Override + public void deleteAlertDefines(Set<Long> alertIds) throws RuntimeException { + alertDefineDao.deleteAlertDefinesByIdIn(alertIds); + for (Long alertId : alertIds) { + periodicAlertRuleScheduler.cancelSchedule(alertId); + } + CacheFactory.clearAlertDefineCache(); + } + + @Override + public Page<AlertDefine> getAlertDefines(List<Long> defineIds, String search, String sort, String order, int pageIndex, int pageSize) { + // parse translation content list + List<String> searchList = Collections.emptyList(); + if (StringUtils.hasText(search)) { + searchList = JsonUtil.fromJson(URLDecoder.decode(search, StandardCharsets.UTF_8), new TypeReference<>() {}); + } + List<String> finalSearchList = searchList; + // build search condition + Specification<AlertDefine> specification = (root, query, criteriaBuilder) -> { + List<Predicate> andList = new ArrayList<>(); + if (defineIds != null && !defineIds.isEmpty()) { + CriteriaBuilder.In<Long> inPredicate = criteriaBuilder.in(root.get("id")); + for (long id : defineIds) { + inPredicate.value(id); + } + andList.add(inPredicate); + } + if (null != finalSearchList && !finalSearchList.isEmpty()) { + List<Predicate> searchPredicates = new ArrayList<>(); + for (String searchContent : finalSearchList) { + searchContent = searchContent.toLowerCase(); + Predicate predicate = criteriaBuilder.or( + criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%" + searchContent + "%"), + criteriaBuilder.like(criteriaBuilder.lower(root.get("expr")), "%" + searchContent + "%"), + criteriaBuilder.like(criteriaBuilder.lower(root.get("labels")), "%" + searchContent + "%"), + criteriaBuilder.like(criteriaBuilder.lower(root.get("annotations")), "%" + searchContent + "%"), + criteriaBuilder.like(criteriaBuilder.lower(root.get("template")), "%" + searchContent + "%") + ); + searchPredicates.add(predicate); + } + // all search keywords are connected with or + andList.add(criteriaBuilder.or(searchPredicates.toArray(new Predicate[0]))); + } + Predicate[] predicates = new Predicate[andList.size()]; + return criteriaBuilder.and(andList.toArray(predicates)); + }; + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort)); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); + return alertDefineDao.findAll(specification, pageRequest); + } + + @Override + public void export(List<Long> ids, String type, HttpServletResponse res) throws Exception { + var imExportService = alertDefineImExportServiceMap.get(type); + if (imExportService == null) { + throw new IllegalArgumentException("not support export type: " + type); + } + var fileName = imExportService.getFileName(); + res.setHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE); + res.setContentType(CONTENT_TYPE); + res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)); + res.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION); + imExportService.exportConfig(res.getOutputStream(), ids); + } + + @Override + public void importConfig(MultipartFile file) throws Exception { + + var type = FileUtil.getFileType(file); + var fileName = FileUtil.getFileName(file); + if (!alertDefineImExportServiceMap.containsKey(type)) { + throw new RuntimeException(ExportFileConstants.FILE + " " + fileName + " is not supported."); + } + var imExportService = alertDefineImExportServiceMap.get(type); + imExportService.importConfig(file.getInputStream()); + } + + @Override + public List<AlertDefine> getMetricsRealTimeAlertDefines() { + List<AlertDefine> alertDefines = CacheFactory.getMetricsAlertDefineCache(); + if (alertDefines == null) { + alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME); + CacheFactory.setMetricsAlertDefineCache(alertDefines); + } + return alertDefines; + } + + @Override + public List<AlertDefine> getLogRealTimeAlertDefines() { + List<AlertDefine> alertDefines = CacheFactory.getLogAlertDefineCache(); + if (alertDefines == null) { + alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME); + CacheFactory.setLogAlertDefineCache(alertDefines); + } + return alertDefines; + } + + @Override + public List<Map<String, Object>> getDefinePreview(String datasource, String type, String expr) { + if (!StringUtils.hasText(expr) || !StringUtils.hasText(datasource) || !StringUtils.hasText(type)) { + return Collections.emptyList(); + } + switch (type) { + case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC: + return dataSourceService.calculate(datasource, expr); + case CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC: + // todo support alert expr preview + return dataSourceService.query(datasource, expr); + default: + log.error("Get define preview unsupported type: {}", type); + return Collections.emptyList(); + } + } + + @Override + public List<AlertDefine> getAlertDefinesByType(String type) { + if (!StringUtils.hasText(type)) { + throw new IllegalArgumentException("Alert definition type cannot be null or empty"); + } + + switch (type) { + case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME: + case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC: + case CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME: + case CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC: + // Valid type, proceed with query + break; + default: + throw new IllegalArgumentException("Unsupported alert definition type: " + type); + } + + // Query enabled alert definitions by type + return alertDefineDao.findAlertDefinesByTypeAndEnableTrue(type); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineYamlImExportServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineYamlImExportServiceImpl.java new file mode 100644 index 0000000..1cee5dd --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineYamlImExportServiceImpl.java @@ -0,0 +1,61 @@ +/* + * 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.service.impl; + +import static org.apache.hertzbeat.common.constants.ExportFileConstants.YamlFile.FILE_SUFFIX; +import static org.apache.hertzbeat.common.constants.ExportFileConstants.YamlFile.TYPE; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO; +import org.springframework.stereotype.Service; +import org.apache.hertzbeat.common.util.export.YamlExportUtils; +import org.yaml.snakeyaml.Yaml; + +/** + * Configure the import and export Yaml format. + */ + +@Slf4j +@Service +public class AlertDefineYamlImExportServiceImpl extends AlertDefineAbstractImExportServiceImpl { + + @Override + public String type() { + return TYPE; + } + + @Override + public String getFileName() { + return fileNamePrefix() + FILE_SUFFIX; + } + + @Override + public List<ExportAlertDefineDTO> parseImport(InputStream is) { + Yaml yaml = new Yaml(); + return yaml.load(is); + } + + @Override + public void writeOs(List<ExportAlertDefineDTO> exportAlertDefineList, OutputStream os) { + + YamlExportUtils.exportWriteOs(exportAlertDefineList, os); + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImpl.java new file mode 100644 index 0000000..c09df75 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImpl.java @@ -0,0 +1,126 @@ +/* + * 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.service.impl; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao; +import org.apache.hertzbeat.alert.reduce.AlarmGroupReduce; +import org.apache.hertzbeat.alert.service.AlertGroupConvergeService; +import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.apache.hertzbeat.common.util.ResourceBundleUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +/** + * implement for alert converge service + */ +@Service +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class AlertGroupConvergeServiceImpl implements AlertGroupConvergeService { + + @Autowired + private AlertGroupConvergeDao alertGroupConvergeDao; + + @Autowired + private AlarmGroupReduce alarmGroupReduce; + + protected ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter"); + + @Override + public void validate(AlertGroupConverge alertGroupConverge, boolean isModify) throws IllegalArgumentException { + if (alertGroupConverge == null || !StringUtils.hasText(alertGroupConverge.getName())) { + throw new IllegalArgumentException(bundle.getString("alerter.converge.empty.name")); + } + List<AlertGroupConverge> sameNameConverges = alertGroupConvergeDao.findAlertGroupConvergesByName(alertGroupConverge.getName()); + if (sameNameConverges != null && !sameNameConverges.isEmpty()) { + boolean isDuplicate = sameNameConverges.stream().anyMatch(it -> !it.getId().equals(alertGroupConverge.getId())); + if (isDuplicate) { + throw new IllegalArgumentException(bundle.getString("alerter.converge.duplicate.name")); + } + } + } + + @Override + public void addAlertGroupConverge(AlertGroupConverge alertGroupConverge) throws RuntimeException { + alertGroupConvergeDao.save(alertGroupConverge); + refreshAlertGroupConvergesCache(); + } + + @Override + public void modifyAlertGroupConverge(AlertGroupConverge alertGroupConverge) throws RuntimeException { + alertGroupConvergeDao.save(alertGroupConverge); + refreshAlertGroupConvergesCache(); + } + + @Override + public AlertGroupConverge getAlertGroupConverge(long convergeId) throws RuntimeException { + return alertGroupConvergeDao.findById(convergeId).orElse(null); + } + + @Override + public void deleteAlertGroupConverges(Set<Long> convergeIds) throws RuntimeException { + alertGroupConvergeDao.deleteAlertGroupConvergesByIdIn(convergeIds); + refreshAlertGroupConvergesCache(); + } + + @Override + public Page<AlertGroupConverge> getAlertGroupConverges(List<Long> convergeIds, String search, String sort, String order, int pageIndex, int pageSize) { + Specification<AlertGroupConverge> specification = (root, query, criteriaBuilder) -> { + List<Predicate> andList = new ArrayList<>(); + if (convergeIds != null && !convergeIds.isEmpty()) { + CriteriaBuilder.In<Long> inPredicate = criteriaBuilder.in(root.get("id")); + for (long id : convergeIds) { + inPredicate.value(id); + } + andList.add(inPredicate); + } + if (StringUtils.hasText(search)) { + Predicate predicate = criteriaBuilder.or( + criteriaBuilder.like( + criteriaBuilder.lower(root.get("name")), + "%" + search.toLowerCase() + "%" + ) + ); + andList.add(predicate); + } + Predicate[] predicates = new Predicate[andList.size()]; + return criteriaBuilder.and(andList.toArray(predicates)); + }; + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort)); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); + return alertGroupConvergeDao.findAll(specification, pageRequest); + } + + private void refreshAlertGroupConvergesCache() { + List<AlertGroupConverge> alertGroupConverges = alertGroupConvergeDao.findAlertGroupConvergesByEnableIsTrue(); + alarmGroupReduce.refreshGroupDefines(alertGroupConverges); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertInhibitServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertInhibitServiceImpl.java new file mode 100644 index 0000000..8fa0f8b --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertInhibitServiceImpl.java @@ -0,0 +1,112 @@ +/* + * 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.service.impl; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.AlertInhibitDao; +import org.apache.hertzbeat.alert.reduce.AlarmInhibitReduce; +import org.apache.hertzbeat.alert.service.AlertInhibitService; +import org.apache.hertzbeat.common.entity.alerter.AlertInhibit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +/** + * management interface service implement for alert inhibit + */ +@Service +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class AlertInhibitServiceImpl implements AlertInhibitService { + + @Autowired + private AlertInhibitDao alertInhibitDao; + + @Autowired + private AlarmInhibitReduce alarmInhibitReduce; + + @Override + public void validate(AlertInhibit alertInhibit, boolean isModify) throws IllegalArgumentException { + // todo + } + + @Override + public void addAlertInhibit(AlertInhibit alertInhibit) throws RuntimeException { + alertInhibitDao.save(alertInhibit); + refreshAlertInhibitsCache(); + } + + @Override + public void modifyAlertInhibit(AlertInhibit alertInhibit) throws RuntimeException { + alertInhibitDao.save(alertInhibit); + refreshAlertInhibitsCache(); + } + + @Override + public AlertInhibit getAlertInhibit(long inhibitId) throws RuntimeException { + return alertInhibitDao.findById(inhibitId).orElse(null); + } + + @Override + public void deleteAlertInhibits(Set<Long> inhibitIds) throws RuntimeException { + alertInhibitDao.deleteAlertInhibitsByIdIn(inhibitIds); + refreshAlertInhibitsCache(); + } + + @Override + public Page<AlertInhibit> getAlertInhibits(List<Long> inhibitIds, String search, String sort, String order, int pageIndex, int pageSize) { + Specification<AlertInhibit> specification = (root, query, criteriaBuilder) -> { + List<Predicate> andList = new ArrayList<>(); + if (inhibitIds != null && !inhibitIds.isEmpty()) { + CriteriaBuilder.In<Long> inPredicate = criteriaBuilder.in(root.get("id")); + for (long id : inhibitIds) { + inPredicate.value(id); + } + andList.add(inPredicate); + } + if (StringUtils.hasText(search)) { + Predicate predicate = criteriaBuilder.or( + criteriaBuilder.like( + criteriaBuilder.lower(root.get("name")), + "%" + search.toLowerCase() + "%" + ) + ); + andList.add(predicate); + } + Predicate[] predicates = new Predicate[andList.size()]; + return criteriaBuilder.and(andList.toArray(predicates)); + }; + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort)); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); + return alertInhibitDao.findAll(specification, pageRequest); + } + + private void refreshAlertInhibitsCache() { + alarmInhibitReduce.refreshInhibitRules(alertInhibitDao.findAlertInhibitsByEnableIsTrue()); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertManagerExternAlertService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertManagerExternAlertService.java new file mode 100644 index 0000000..8433cf4 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertManagerExternAlertService.java @@ -0,0 +1,105 @@ +/* + * 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.service.impl; + +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dto.AlertManagerExternAlert; +import org.apache.hertzbeat.alert.dto.PrometheusExternAlert; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.ExternAlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +/** + * Alertmanager external alarm service impl + */ +@Slf4j +@Service +public class AlertManagerExternAlertService implements ExternAlertService { + + @Autowired + private AlarmCommonReduce alarmCommonReduce; + + + @Override + public void addExternAlert(String content) { + + AlertManagerExternAlert alert = JsonUtil.fromJson(content, AlertManagerExternAlert.class); + if (alert == null) { + log.warn("parse alertmanager extern alert content failed! content: {}", content); + return; + } + List<PrometheusExternAlert> alerts = alert.getAlerts(); + if (alerts == null || alerts.isEmpty()) { + log.warn("receive alertmanager extern alert without alerts! content: {}", content); + return; + } + for (PrometheusExternAlert prometheusAlert : alerts) { + Map<String, String> annotations = prometheusAlert.getAnnotations(); + if (annotations == null) { + annotations = new HashMap<>(8); + } + if (StringUtils.hasText(prometheusAlert.getGeneratorURL())) { + annotations.put("generatorURL", prometheusAlert.getGeneratorURL()); + } + String description = annotations.get("description"); + if (description == null) { + description = annotations.get("summary"); + } + if (description == null) { + description = annotations.values().stream().findFirst().orElse(""); + } + Map<String, String> labels = prometheusAlert.getLabels(); + if (labels == null) { + labels = new HashMap<>(8); + } + labels.put("__source__", "alertmanager"); + String status = CommonConstants.ALERT_STATUS_FIRING; + Instant now = Instant.now(); + Instant endsAt = prometheusAlert.getEndsAt(); + if (endsAt != null && endsAt.getEpochSecond() > 0 && endsAt.isBefore(now)) { + status = CommonConstants.ALERT_STATUS_RESOLVED; + } + SingleAlert singleAlert = SingleAlert.builder() + .content(description) + .status(status) + .activeAt(CommonConstants.ALERT_STATUS_FIRING.equals(status) ? Instant.now().toEpochMilli() : null) + .startAt(prometheusAlert.getStartsAt() != null ? prometheusAlert.getStartsAt().toEpochMilli() : Instant.now().toEpochMilli()) + .endAt(CommonConstants.ALERT_STATUS_RESOLVED.equals(status) ? prometheusAlert.getEndsAt().toEpochMilli() : null) + .labels(labels) + .annotations(prometheusAlert.getAnnotations()) + .triggerTimes(1) + .build(); + + alarmCommonReduce.reduceAndSendAlarm(singleAlert); + } + } + + @Override + public String supportSource() { + return "alertmanager"; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java new file mode 100644 index 0000000..80bb952 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java @@ -0,0 +1,206 @@ +/* + * 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.service.impl; + +import jakarta.persistence.criteria.Predicate; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.GroupAlertDao; +import org.apache.hertzbeat.alert.dao.SingleAlertDao; +import org.apache.hertzbeat.alert.dto.AlertSummary; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.AlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Realization of Alarm Information Service + */ +@Service +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class AlertServiceImpl implements AlertService { + + @Autowired + private GroupAlertDao groupAlertDao; + + @Autowired + private SingleAlertDao singleAlertDao; + + @Autowired + private AlarmCommonReduce alarmCommonReduce; + + @Override + public Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) { + Specification<SingleAlert> specification = (root, query, criteriaBuilder) -> { + List<Predicate> andList = new ArrayList<>(); + if (status != null) { + Predicate predicate = criteriaBuilder.equal(root.get("status"), status); + andList.add(predicate); + } + Predicate[] andPredicates = new Predicate[andList.size()]; + Predicate andPredicate = criteriaBuilder.and(andList.toArray(andPredicates)); + List<Predicate> orList = new ArrayList<>(); + if (search != null && !search.isEmpty()) { + Predicate predicateContent = criteriaBuilder.like(root.get("content"), "%" + search + "%"); + orList.add(predicateContent); + Predicate predicateLabels = criteriaBuilder.like(root.get("labels"), "%" + search + "%"); + orList.add(predicateLabels); + Predicate predicateAnnotation = criteriaBuilder.like(root.get("annotations"), "%" + search + "%"); + orList.add(predicateAnnotation); + } + Predicate[] orPredicates = new Predicate[orList.size()]; + Predicate orPredicate = criteriaBuilder.or(orList.toArray(orPredicates)); + if (andPredicates.length == 0 && orPredicates.length == 0) { + return query.where().getRestriction(); + } else if (andPredicates.length == 0) { + return orPredicate; + } else if (orPredicates.length == 0) { + return andPredicate; + } else { + return query.where(andPredicate, orPredicate).getRestriction(); + } + }; + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort)); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); + return singleAlertDao.findAll(specification, pageRequest); + } + + @Override + public Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) { + Specification<GroupAlert> specification = (root, query, criteriaBuilder) -> { + List<Predicate> andList = new ArrayList<>(); + if (status != null) { + Predicate predicate = criteriaBuilder.equal(root.get("status"), status); + andList.add(predicate); + } + Predicate[] andPredicates = new Predicate[andList.size()]; + Predicate andPredicate = criteriaBuilder.and(andList.toArray(andPredicates)); + List<Predicate> orList = new ArrayList<>(); + if (search != null && !search.isEmpty()) { + Predicate predicateContent = criteriaBuilder.like(root.get("groupLabels"), "%" + search + "%"); + orList.add(predicateContent); + Predicate predicateLabels = criteriaBuilder.like(root.get("commonLabels"), "%" + search + "%"); + orList.add(predicateLabels); + Predicate predicateAnnotation = criteriaBuilder.like(root.get("commonAnnotations"), "%" + search + "%"); + orList.add(predicateAnnotation); + } + Predicate[] orPredicates = new Predicate[orList.size()]; + Predicate orPredicate = criteriaBuilder.or(orList.toArray(orPredicates)); + if (andPredicates.length == 0 && orPredicates.length == 0) { + return query.where().getRestriction(); + } else if (andPredicates.length == 0) { + return orPredicate; + } else if (orPredicates.length == 0) { + return andPredicate; + } else { + return query.where(andPredicate, orPredicate).getRestriction(); + } + }; + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort)); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); + Page<GroupAlert> groupAlertPage = groupAlertDao.findAll(specification, pageRequest); + for (GroupAlert groupAlert : groupAlertPage.getContent()) { + List<String> firingAlerts = groupAlert.getAlertFingerprints(); + List<SingleAlert> singleAlerts = singleAlertDao.findSingleAlertsByFingerprintIn(firingAlerts); + groupAlert.setAlerts(singleAlerts); + } + return groupAlertPage; + } + + @Override + public void deleteGroupAlerts(HashSet<Long> ids) { + List<GroupAlert> groupAlerts = groupAlertDao.findGroupAlertsByIdIn(ids); + for (GroupAlert groupAlert : groupAlerts) { + List<String> firingAlerts = groupAlert.getAlertFingerprints(); + singleAlertDao.deleteSingleAlertsByFingerprintIn(firingAlerts); + } + groupAlertDao.deleteGroupAlertsByIdIn(ids); + } + + @Override + public void deleteSingleAlerts(HashSet<Long> ids) { + singleAlertDao.deleteSingleAlertsByIdIn(ids); + } + + @Override + public void editGroupAlertStatus(String status, List<Long> ids) { + groupAlertDao.updateGroupAlertsStatus(status, ids); + } + + @Override + public void editSingleAlertStatus(String status, List<Long> ids) { + singleAlertDao.updateSingleAlertsStatus(status, ids); + } + + + @Override + public AlertSummary getAlertsSummary() { + AlertSummary alertSummary = new AlertSummary(); + // Statistics on the alarm information in the alarm state + List<SingleAlert> firingAlerts = singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING); + // severity - emergency critical warning info + int emergencyNum = 0; + int criticalNum = 0; + int warningNum = 0; + for (SingleAlert alert : firingAlerts) { + String severity = alert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY); + if (severity != null) { + switch (severity) { + case CommonConstants.ALERT_SEVERITY_EMERGENCY -> emergencyNum++; + case CommonConstants.ALERT_SEVERITY_CRITICAL -> criticalNum++; + case CommonConstants.ALERT_SEVERITY_WARNING -> warningNum++; + default -> {} + } + } + alertSummary.setPriorityCriticalNum(criticalNum); + alertSummary.setPriorityEmergencyNum(emergencyNum); + alertSummary.setPriorityWarningNum(warningNum); + } + + long total = singleAlertDao.count(); + alertSummary.setTotal(total); + long resolved = total - firingAlerts.size(); + alertSummary.setDealNum(resolved); + try { + if (total == 0) { + alertSummary.setRate(100); + } else { + float rate = BigDecimal.valueOf(100 * (float) resolved / total) + .setScale(2, RoundingMode.HALF_UP) + .floatValue(); + alertSummary.setRate(rate); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return alertSummary; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertSilenceServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertSilenceServiceImpl.java new file mode 100644 index 0000000..c989b1a --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertSilenceServiceImpl.java @@ -0,0 +1,114 @@ +/* + * 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.service.impl; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.AlertSilenceDao; +import org.apache.hertzbeat.alert.service.AlertSilenceService; +import org.apache.hertzbeat.common.cache.CacheFactory; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +/** + * management interface service implement for alert silence + */ +@Service +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class AlertSilenceServiceImpl implements AlertSilenceService { + + @Autowired + private AlertSilenceDao alertSilenceDao; + + @Override + public void validate(AlertSilence alertSilence, boolean isModify) throws IllegalArgumentException { + // todo + // if the alarm silent selection date set in periodic situations is empty, it will be deemed to be all checked. + if (alertSilence.getType() == 1 && alertSilence.getDays() == null) { + alertSilence.setDays(Arrays.asList((byte) 7, (byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5, (byte) 6)); + } + } + + @Override + public void addAlertSilence(AlertSilence alertSilence) throws RuntimeException { + alertSilenceDao.save(alertSilence); + clearAlertSilencesCache(); + } + + @Override + public void modifyAlertSilence(AlertSilence alertSilence) throws RuntimeException { + alertSilenceDao.save(alertSilence); + clearAlertSilencesCache(); + } + + @Override + public AlertSilence getAlertSilence(long silenceId) throws RuntimeException { + return alertSilenceDao.findById(silenceId).orElse(null); + } + + @Override + public void deleteAlertSilences(Set<Long> silenceIds) throws RuntimeException { + alertSilenceDao.deleteAlertSilencesByIdIn(silenceIds); + clearAlertSilencesCache(); + } + + @Override + public Page<AlertSilence> getAlertSilences(List<Long> silenceIds, String search, String sort, String order, int pageIndex, int pageSize) { + Specification<AlertSilence> specification = (root, query, criteriaBuilder) -> { + List<Predicate> andList = new ArrayList<>(); + if (silenceIds != null && !silenceIds.isEmpty()) { + CriteriaBuilder.In<Long> inPredicate = criteriaBuilder.in(root.get("id")); + for (long id : silenceIds) { + inPredicate.value(id); + } + andList.add(inPredicate); + } + if (StringUtils.hasText(search)) { + Predicate predicate = criteriaBuilder.or( + criteriaBuilder.like( + criteriaBuilder.lower(root.get("name")), + "%" + search.toLowerCase() + "%" + ) + ); + andList.add(predicate); + } + Predicate[] predicates = new Predicate[andList.size()]; + return criteriaBuilder.and(andList.toArray(predicates)); + }; + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort)); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); + return alertSilenceDao.findAll(specification, pageRequest); + } + + private void clearAlertSilencesCache() { + CacheFactory.clearAlertSilenceCache(); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlibabaCloudSlsExternAlertService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlibabaCloudSlsExternAlertService.java new file mode 100644 index 0000000..a3019b3 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlibabaCloudSlsExternAlertService.java @@ -0,0 +1,202 @@ +/* + * 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.service.impl; + +import tools.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.dto.AlibabaCloudSlsExternAlert; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.ExternAlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.stereotype.Service; + +import java.text.MessageFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Alibaba Cloud 'Simple Log Service(SLS)' external alarm service impl + */ +@Slf4j +@Service +public class AlibabaCloudSlsExternAlertService implements ExternAlertService { + + private final AlarmCommonReduce alarmCommonReduce; + + private static final AlibabaCloudSlsConverter CONVERTER = new AlibabaCloudSlsConverter(); + + public AlibabaCloudSlsExternAlertService(AlarmCommonReduce alarmCommonReduce) { + this.alarmCommonReduce = alarmCommonReduce; + } + + @Override + public void addExternAlert(String content) { + List<AlibabaCloudSlsExternAlert> externAlerts = new ArrayList<>(); + if (BooleanUtils.isTrue(JsonUtil.isArray(content))) { + TypeReference<List<AlibabaCloudSlsExternAlert>> typeReference = new TypeReference<>() {}; + externAlerts = JsonUtil.fromJson(content, typeReference); + } else { + AlibabaCloudSlsExternAlert externAlert = JsonUtil.fromJson(content, AlibabaCloudSlsExternAlert.class); + if (null != externAlert) { + externAlerts.add(externAlert); + } + } + if (null == externAlerts || externAlerts.isEmpty()) { + log.warn("Failure to parse external alert content. content: {}", content); + return; + } + for (AlibabaCloudSlsExternAlert externAlert : externAlerts) { + SingleAlert singleAlert = CONVERTER.convert(externAlert); + alarmCommonReduce.reduceAndSendAlarm(singleAlert); + } + } + + @Override + public String supportSource() { + return "alibabacloud-sls"; + } + + /** + * + */ + public static class AlibabaCloudSlsConverter { + + /** + * convert + * + * @param externAlert alert content entity + * @return Single alert + */ + public SingleAlert convert(AlibabaCloudSlsExternAlert externAlert) { + return SingleAlert.builder() + .triggerTimes(1) + .status(externAlert.getStatus()) + .startAt(Instant.ofEpochSecond(externAlert.getFireTime()).toEpochMilli()) + .activeAt(Instant.ofEpochSecond(externAlert.getAlertTime()).toEpochMilli()) + .endAt(convertResolveTime(externAlert.getStatus(), externAlert.getResolveTime())) + .labels(buildLabels(externAlert)) + .annotations(buildAnnotations(externAlert)) + .content(formatContent(externAlert)) + .build(); + } + + /** + * todo i18n + * + * @param externAlert alert content entity + * @return content + */ + private String formatContent(AlibabaCloudSlsExternAlert externAlert) { + // convet severity + Optional<AlibabaCloudSlsExternAlert.Severity> severity = AlibabaCloudSlsExternAlert.Severity.convert(externAlert.getSeverity()); + // If the alarm state is resolved, the value is the specific recovery time. + Long resolveTimeMilli = convertResolveTime(externAlert.getStatus(), externAlert.getResolveTime()); + + return MessageFormat.format( + "AlibabaCloud-sls alert , {0} - [{1}], level: [{2}], desc: {3}, fire_time:{4}, resolve_time:{5}", + externAlert.getAnnotation("title"), + externAlert.getStatus(), + severity.isPresent() ? severity.get().getAlias() : "N/A", + externAlert.getAnnotation("desc"), + timeSecondToDate(Instant.ofEpochSecond(externAlert.getFireTime()).toEpochMilli()), + null != resolveTimeMilli ? timeSecondToDate(resolveTimeMilli) : "N/A" + ); + } + + /** + * Converts a timestamp (milliseconds) to a formatted date-time string. + * + * @param timestampMillis timestamp in milliseconds + * @return formatted date-time string in the pattern: yyyy-MM-dd HH:mm:ss + */ + private String timeSecondToDate(long timestampMillis) { + LocalDateTime dateTime = LocalDateTime.ofInstant( + Instant.ofEpochMilli(timestampMillis), + ZoneId.systemDefault() + ); + return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + + /** + * Build basic annotations and fill annotations for alibaba cloud sls. + * + * @param externAlert alert content entity + * @return annotations + */ + private Map<String, String> buildAnnotations(AlibabaCloudSlsExternAlert externAlert) { + Map<String, String> annotations = new HashMap<>(8); + Optional<AlibabaCloudSlsExternAlert.Severity> severity = AlibabaCloudSlsExternAlert.Severity.convert(externAlert.getSeverity()); + severity.ifPresent(value -> annotations.put("severity", value.getAlias())); + // Notification templates for sls need to be configured. + if (StringUtils.isNotBlank(externAlert.getSigninUrl()) && IpDomainUtil.isHasSchema(externAlert.getSigninUrl())) { + annotations.put("signinUrl", "<a target=\"_blank\" href=\"" + externAlert.getSigninUrl() + "\">View Details</a>"); + } + // Filling the annotations with the alibaba cloud sls. + if (null != externAlert.getAnnotations() && !externAlert.getAnnotations().isEmpty()) { + annotations.putAll(externAlert.getAnnotations()); + } + + return annotations; + } + + /** + * Build basic labels and fill labels for alibaba cloud sls. + * + * @param externAlert alert content entity + * @return labels + */ + private Map<String, String> buildLabels(AlibabaCloudSlsExternAlert externAlert) { + Map<String, String> labels = new HashMap<>(8); + labels.put("__source__", "alibabacloud-sls"); + labels.put("alertname", externAlert.getAlertName()); + labels.put("region", externAlert.getRegion()); + // The project name is globally unique. + labels.put("project", externAlert.getProject()); + // Filling the labels with the alibaba cloud sls. + if (null != externAlert.getLabels() && !externAlert.getLabels().isEmpty()){ + labels.putAll(externAlert.getLabels()); + } + return labels; + } + + /** + * If the alarm status is firing, the value is 0. + * If the alarm state is resolved, the value is the specific recovery time. + * + * @param status alert status + * @param resolveTimeSecond recovery time + * @return milliseconds + */ + private Long convertResolveTime(String status, int resolveTimeSecond) { + return CommonConstants.ALERT_STATUS_RESOLVED.equals(status) ? Instant.ofEpochSecond(resolveTimeSecond).toEpochMilli() : null; + } + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlibabaSmsClientImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlibabaSmsClientImpl.java new file mode 100644 index 0000000..34b6ebe --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlibabaSmsClientImpl.java @@ -0,0 +1,239 @@ +/* + * 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.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.sms.AlibabaSmsProperties; +import org.apache.hertzbeat.alert.service.SmsClient; +import org.apache.hertzbeat.alert.util.CryptoUtils; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.support.exception.SendMessageException; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.common.util.LogUtil; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.SimpleTimeZone; +import java.util.TreeMap; +import java.util.UUID; +import tools.jackson.databind.JsonNode; + +import static org.apache.hertzbeat.common.constants.SmsConstants.ALIBABA; + +/** + * Alibaba Cloud SMS Client Implementation<br> + * API doc: <a href="https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms">https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms</a><br> + * Singnature doc: <a href="https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature">https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature</a> + */ +@Slf4j +public class AlibabaSmsClientImpl implements SmsClient { + + private static final String API_VERSION = "2017-05-25"; + private static final String ACTION = "SendSms"; + private static final String HOST = "dysmsapi.aliyuncs.com"; + private static final String ALGORITHM = "ACS3-HMAC-SHA256"; + + private final String accessKeyId; + private final String accessKeySecret; + private final String signName; + private final String templateCode; + private static final Logger logger = LoggerFactory.getLogger(AlibabaSmsClientImpl.class); + + public AlibabaSmsClientImpl(AlibabaSmsProperties config) { + if (config != null) { + this.accessKeyId = config.getAccessKeyId(); + this.accessKeySecret = config.getAccessKeySecret(); + this.signName = config.getSignName(); + this.templateCode = config.getTemplateCode(); + } else { + this.accessKeyId = ""; + this.accessKeySecret = ""; + this.signName = ""; + this.templateCode = ""; + } + } + + @Override + public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + // Extract alert info + String instance = null; + String priority = null; + String content = null; + if (alert.getCommonLabels() != null) { + instance = alert.getCommonLabels().get("instance"); + priority = alert.getCommonLabels().get("priority"); + content = alert.getCommonAnnotations().get("summary"); + content = content == null ? alert.getCommonAnnotations().get("description") : content; + if (content == null) { + content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null); + } + } + + // Build template parameters + Map<String, String> templateParam = new HashMap<>(); + templateParam.put("instance", instance == null ? alert.getGroupKey() : instance); + templateParam.put("priority", priority == null ? "unknown" : priority); + templateParam.put("content", content); + + sendSms(receiver.getPhone(), JsonUtil.toJson(templateParam)); + } + + private void sendSms(String phoneNumber, String templateParam) { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + // Build query parameters + Map<String, String> queryParams = new TreeMap<>(); + queryParams.put("PhoneNumbers", phoneNumber); + queryParams.put("SignName", signName); + queryParams.put("TemplateCode", templateCode); + queryParams.put("TemplateParam", templateParam); + + // Build canonical query string + StringBuilder canonicalQueryString = new StringBuilder(); + queryParams.forEach((key, value) -> { + if (canonicalQueryString.length() > 0) { + canonicalQueryString.append("&"); + } + canonicalQueryString.append(percentEncode(key)) + .append("=") + .append(percentEncode(value)); + }); + + // Generate timestamp and nonce + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + sdf.setTimeZone(new SimpleTimeZone(0, "GMT")); + String timestamp = sdf.format(new Date()); + String nonce = UUID.randomUUID().toString(); + + // Calculate signature + String authorization = calculateAuthorization( + canonicalQueryString.toString(), + timestamp, + nonce + ); + + // Build URL + String url = "https://" + HOST + "/?" + canonicalQueryString; + + // Build HTTP request + HttpPost httpPost = new HttpPost(url); + httpPost.setHeader("Content-Type", "application/json"); + httpPost.setHeader("Host", HOST); + httpPost.setHeader("Authorization", authorization); + httpPost.setHeader("x-acs-action", ACTION); + httpPost.setHeader("x-acs-version", API_VERSION); + httpPost.setHeader("x-acs-date", timestamp); + httpPost.setHeader("x-acs-signature-nonce", nonce); + httpPost.setHeader("x-acs-content-sha256", + CryptoUtils.sha256Hex("")); + + log.info("Sending Alibaba SMS request to {}", url + ", params: " + templateParam + "headers: " + Arrays.toString(httpPost.getAllHeaders())); + + // Send request and handle response + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + + log.info("SMS response status: {}, body: {}", statusCode, responseBody); + + if (statusCode != 200) { + throw new SendMessageException("HTTP request failed with status code: " + statusCode + ", response: " + responseBody); + } + + JsonNode jsonResponse = JsonUtil.fromJson(responseBody); + String code = jsonResponse.get("Code").asText(); + if (!"OK".equals(code)) { + String message = jsonResponse.get("Message").asText(); + throw new SendMessageException(code + ":" + message); + } + + log.info("Successfully sent SMS to phone: {}", phoneNumber); + } + } catch (Exception e) { + LogUtil.warn(logger, "Failed to send SMS: {0}", e.getMessage()); + throw new SendMessageException(e.getMessage()); + } + } + + private String calculateAuthorization(String canonicalQueryString, String timestamp, String nonce) { + try { + // Step 1: Build canonical request + String canonicalRequest = buildCanonicalRequest(canonicalQueryString, timestamp, nonce); + + // Step 2: Build string to sign + String stringToSign = ALGORITHM + "\n" + CryptoUtils.sha256Hex(canonicalRequest); + + // Step 3: Calculate signature + String signature = CryptoUtils.hmacSha256Hex(accessKeySecret, stringToSign); + + // Step 4: Build authorization header + return ALGORITHM + " Credential=" + accessKeyId + ",SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;" + "x-acs-signature-nonce;x-acs-version,Signature=" + signature; + } catch (Exception e) { + LogUtil.warn(logger, "Failed to calculate authorization {0}", e.getMessage()); + throw new RuntimeException("Failed to calculate authorization", e); + } + } + + private String buildCanonicalRequest(String canonicalQueryString, String timestamp, String nonce) { + return "POST\n" + + "/\n" + + canonicalQueryString + "\n" + + "host:" + HOST + "\n" + + "x-acs-action:" + ACTION + "\n" + + "x-acs-content-sha256:" + CryptoUtils.sha256Hex("") + "\n" + + "x-acs-date:" + timestamp + "\n" + + "x-acs-signature-nonce:" + nonce + "\n" + + "x-acs-version:" + API_VERSION + "\n\n" + + "host;x-acs-action;x-acs-content-sha256;x-acs-date;" + + "x-acs-signature-nonce;x-acs-version\n" + + CryptoUtils.sha256Hex(""); + } + + private String percentEncode(String value) { + try { + return java.net.URLEncoder.encode(value, StandardCharsets.UTF_8) + .replace("+", "%20") + .replace("*", "%2A") + .replace("%7E", "~"); + } catch (Exception e) { + throw new RuntimeException("Failed to encode value", e); + } + } + + @Override + public String getType() { + return ALIBABA; + } + + @Override + public boolean checkConfig() { + return !(accessKeyId.isBlank() || accessKeySecret.isBlank() || signName.isBlank() || templateCode.isBlank()); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AwsSmsClientImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AwsSmsClientImpl.java new file mode 100644 index 0000000..6e9dd60 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AwsSmsClientImpl.java @@ -0,0 +1,289 @@ +/* + * 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.service.impl; + +import tools.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.sms.AwsSmsProperties; +import org.apache.hertzbeat.alert.service.SmsClient; +import org.apache.hertzbeat.alert.util.CryptoUtils; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.support.exception.SendMessageException; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.apache.hertzbeat.common.constants.SmsConstants.AWS; + +/** + * AWS Cloud SMS Client Implementation<br> + * API doc:<a href="https://docs.aws.amazon.com/pinpoint/latest/apireference_smsvoicev2/API_SendTextMessage.html">https://docs.aws.amazon.com/pinpoint/latest/apireference_smsvoicev2/API_SendTextMessage.html</a><br> + * Signature doc:<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html"</a>https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html</a> + */ +@Slf4j +public class AwsSmsClientImpl implements SmsClient { + + + private static final String SERVICE = "sms-voice"; + private static final String MESSAGE_TEMPLATE = "Instance: %s, Priority: %s, Content: %s"; + + private final String accessKey; + private final String secretKey; + private final String region; + + public AwsSmsClientImpl(AwsSmsProperties config) { + if (config != null) { + this.accessKey = config.getAccessKeyId(); + this.secretKey = config.getAccessKeySecret(); + this.region = config.getRegion(); + } else { + this.accessKey = ""; + this.secretKey = ""; + this.region = ""; + } + } + + @Override + public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + // Extract alert info + String instance = null; + String priority = null; + String content = null; + if (alert.getCommonLabels() != null) { + instance = alert.getCommonLabels().get("instance") == null ? alert.getGroupKey() : alert.getCommonLabels().get("instance"); + priority = alert.getCommonLabels().get("priority") == null ? "unknown" : alert.getCommonLabels().get("priority"); + content = alert.getCommonAnnotations().get("summary"); + content = content == null ? alert.getCommonAnnotations().get("description") : content; + if (content == null) { + content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null); + } + } + this.send(receiver.getPhone(), createMessage(instance, priority, content)); + } + + private String createMessage(String instance, String priority, String content) { + return String.format(MESSAGE_TEMPLATE, instance, priority, content); + } + + private void send(String phoneNumber, String message) { + Map<String, Object> payload = new HashMap<>(); + payload.put("MessageBody", message); + payload.put("DestinationPhoneNumber", phoneNumber); + payload.put("MessageType", "TRANSACTIONAL"); + + Instant now = Instant.now(); + String amzDate = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'").withZone(ZoneOffset.UTC).format(now); + String endpoint = "https://" + SERVICE + "." + region + ".amazonaws.com"; + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + String payloadInString = JsonUtil.toJson(payload); + if (payloadInString == null) { + throw new SendMessageException("Failed to serialize SMS payload"); + } + URI requestUri = new URI(endpoint); + + HttpPost httpPost = createHttpPost(requestUri, amzDate, payloadInString); + log.info("Sending AWS SMS request to {}", requestUri + "," + "headers: " + Arrays.toString(httpPost.getAllHeaders())); + executeRequest(httpClient, httpPost, phoneNumber); + } catch (Exception e) { + log.warn("Failed to send SMS: {}", e.getMessage()); + throw new SendMessageException(e.getMessage()); + } + } + + + private HttpPost createHttpPost(URI requestUri, String amzDate, String payloadInString) throws Exception { + // Define headers once to avoid repetition (Order of headers must be maintained) + Map<String, String> headers = new LinkedHashMap<>(); + headers.put("content-type", "application/x-amz-json-1.0"); + headers.put("host", SERVICE + "." + this.region + ".amazonaws.com"); + headers.put("x-amz-date", amzDate); + headers.put("x-amz-target", "PinpointSMSVoiceV2.SendTextMessage"); + + // Build authorization header using the same headers + AwsAuthenticationBuilder authBuilder = new AwsAuthenticationBuilder() + .credential(this.accessKey, this.secretKey) + .region(this.region); + headers.forEach(authBuilder::header); + String authorizationHeader = authBuilder.build(requestUri, "POST", SERVICE, payloadInString); + + // Create HTTP post and set all headers + HttpPost httpPost = new HttpPost(requestUri); + httpPost.setHeader("Authorization", authorizationHeader); + headers.forEach(httpPost::setHeader); + + httpPost.setEntity(new StringEntity(payloadInString, ContentType.APPLICATION_JSON)); + return httpPost; + } + + private void executeRequest(CloseableHttpClient httpClient, HttpPost httpPost, String phoneNumber) throws Exception { + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + log.info("SMS response status: {}, body: {}", statusCode, responseBody); + + if (statusCode != 200) { + throw new SendMessageException("HTTP request failed with status code: " + statusCode + ", response: " + responseBody); + } + + JsonNode jsonResponse = JsonUtil.fromJson(responseBody); + if (jsonResponse == null) { + throw new SendMessageException(statusCode + ":" + responseBody); + } + + JsonNode responseNode = jsonResponse.get("MessageId"); + if (responseNode == null) { + throw new SendMessageException(statusCode + ":" + responseBody); + } + + String messageId = responseNode.asText(); + log.info("Successfully sent SMS to phone: {}, messageId: {}", phoneNumber, messageId); + } + } + + @Override + public String getType() { + return AWS; + } + + @Override + public boolean checkConfig() { + return !(accessKey.isBlank() || secretKey.isBlank() || region.isBlank()); + } + + + private static class AwsAuthenticationBuilder { + private String accessKey; + private String secretKey; + private String region; + private String service; + private URI requestUri; + private String method; + private String payload; + private final Map<String, String> headers = new LinkedHashMap<>(); + + public AwsAuthenticationBuilder region(String region) { + this.region = region; + return this; + } + + public AwsAuthenticationBuilder credential(String accessKey, String secretKey) { + this.accessKey = accessKey; + this.secretKey = secretKey; + return this; + } + + public AwsAuthenticationBuilder header(String header, String value) { + this.headers.put(header, value); + return this; + } + + public String build(URI requestUri, String method, String service, String payload) throws Exception { + this.requestUri = requestUri; + this.method = method; + this.service = service; + this.payload = payload; + return "%s Credential=%s/%s, SignedHeaders=%s, Signature=%s".formatted( + getAlgorithm(), + this.accessKey, + getCredentialScope(), + getSignedHeaders(), + getSignature() + ); + } + + private String getCredentialScope() { + return getDateStamp() + "/" + region + "/" + service + "/aws4_request"; + } + + private String getSignedHeaders() { + return String.join(";", headers.keySet()); + } + + private String getDateTime() { + return headers.get("x-amz-date"); + } + + private String getDateStamp() { + return getDateTime().substring(0, 8); + } + + private String getSignature() { + String canonicalRequest = getCanonicalRequest(); + String stringToSign = String.join("\n", + getAlgorithm(), + getDateTime(), + getCredentialScope(), + CryptoUtils.sha256Hex(canonicalRequest)); + + byte[] signingKey = getSignatureKey(secretKey, getDateStamp(), region, service); + return CryptoUtils.hmacSha256Hex(signingKey, stringToSign); + } + + private String getCanonicalRequest() { + return String.join("\n", + method, + requestUri.getPath().isEmpty() ? "/" : requestUri.getPath(), + requestUri.getQuery() != null ? requestUri.getQuery() : "", + getCanonicalHeaders(), + getSignedHeaders(), + CryptoUtils.sha256Hex(payload) + ); + } + + private String getCanonicalHeaders() { + return headers.entrySet().stream() + .map(entry -> entry.getKey() + ":" + entry.getValue()) + .collect(Collectors.joining("\n")) + "\n"; + } + + private String getAlgorithm() { + return "AWS4-HMAC-SHA256"; + } + + private byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName) { + byte[] secret = ("AWS4" + key).getBytes(StandardCharsets.UTF_8); + byte[] date = CryptoUtils.hmac256(secret, dateStamp); + byte[] region = CryptoUtils.hmac256(date, regionName); + byte[] service = CryptoUtils.hmac256(region, serviceName); + return CryptoUtils.hmac256(service, "aws4_request"); + } + + + } +} + + diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DataSourceServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DataSourceServiceImpl.java new file mode 100644 index 0000000..136777c --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DataSourceServiceImpl.java @@ -0,0 +1,201 @@ +/* + * 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.service.impl; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.ParseTree; +import org.apache.hertzbeat.alert.expr.AlertExpressionEvalVisitor; +import org.apache.hertzbeat.alert.expr.AlertExpressionLexer; +import org.apache.hertzbeat.alert.expr.AlertExpressionParser; +import org.apache.hertzbeat.alert.service.DataSourceService; +import org.apache.hertzbeat.common.support.exception.AlertExpressionException; +import org.apache.hertzbeat.common.support.valid.SqlSecurityException; +import org.apache.hertzbeat.common.support.valid.SqlSecurityValidator; +import org.apache.hertzbeat.common.util.ResourceBundleUtil; +import org.apache.hertzbeat.warehouse.constants.WarehouseConstants; +import org.apache.hertzbeat.warehouse.db.QueryExecutor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.concurrent.TimeUnit; + +/** + * datasource service + */ +@Service +@Slf4j +public class DataSourceServiceImpl implements DataSourceService { + + /** + * Default allowed tables for SQL queries + */ + private static final List<String> DEFAULT_ALLOWED_TABLES = List.of(WarehouseConstants.LOG_TABLE_NAME); + + protected ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter"); + + @Setter + private List<QueryExecutor> executors; + + private final SqlSecurityValidator sqlSecurityValidator; + + @Getter + private final Cache<String, ParseTree> expressionCache = Caffeine.newBuilder() + .maximumSize(256) + .expireAfterAccess(1, TimeUnit.HOURS) + .recordStats() + .build(); + + public DataSourceServiceImpl(@Autowired(required = false) List<QueryExecutor> executors) { + this.executors = executors != null ? executors : Collections.emptyList(); + this.sqlSecurityValidator = new SqlSecurityValidator(DEFAULT_ALLOWED_TABLES); + } + + @Override + public List<Map<String, Object>> calculate(String datasource, String expr) { + if (!StringUtils.hasText(expr)) { + throw new IllegalArgumentException("Empty expression"); + } + if (executors == null || executors.isEmpty()) { + throw new IllegalArgumentException(bundle.getString("alerter.datasource.executor.not.found")); + } + QueryExecutor executor = executors.stream().filter(e -> e.support(datasource)).findFirst().orElse(null); + + if (executor == null) { + throw new IllegalArgumentException("Unsupported datasource: " + datasource); + } + // replace all white space + expr = expr.replaceAll("\\s+", " "); + try { + return evaluate(expr, executor); + } catch (AlertExpressionException ae) { + log.error("Calculate query parse error, datasource: {}, expr: {}, msg: {}", datasource, expr, ae.getMessage(), ae); + throw ae; + } catch (Exception e) { + log.error("Error executing query on datasource {}: {}", datasource, e.getMessage()); + throw new RuntimeException("Query execution failed", e); + } + } + + @Override + public List<Map<String, Object>> query(String datasource, String expr) { + if (!StringUtils.hasText(expr)) { + throw new IllegalArgumentException("Empty expression"); + } + if (executors == null || executors.isEmpty()) { + throw new IllegalArgumentException(bundle.getString("alerter.datasource.executor.not.found")); + } + QueryExecutor executor = executors.stream().filter(e -> e.support(datasource)).findFirst().orElse(null); + + if (executor == null) { + throw new IllegalArgumentException("Unsupported datasource: " + datasource); + } + // replace all white space + expr = expr.replaceAll("\\s+", " "); + + // SQL security validation for SQL-based datasources + if (isSqlDatasource(datasource)) { + validateSqlSecurity(expr); + } + + try { + return executor.execute(expr); + } catch (Exception e) { + log.error("Error executing query on datasource {}: {}", datasource, e.getMessage()); + throw new AlertExpressionException(e.getMessage()); + } + } + + /** + * Check if the datasource is SQL-based + */ + private boolean isSqlDatasource(String datasource) { + return datasource != null && datasource.equalsIgnoreCase(WarehouseConstants.SQL); + } + + /** + * Validate SQL statement for security + */ + private void validateSqlSecurity(String sql) { + try { + sqlSecurityValidator.validate(sql); + } catch (SqlSecurityException e) { + log.warn("SQL security validation failed: {}", e.getMessage()); + throw new AlertExpressionException("SQL security validation failed: " + e.getMessage()); + } + } + + private List<Map<String, Object>> evaluate(String expr, QueryExecutor executor) { + CommonTokenStream tokens = createTokenStream(expr); + AlertExpressionParser parser = new AlertExpressionParser(tokens); + ParseTree tree = expressionCache.get(expr, e -> parser.expr()); + // Validate EOF only during the first parsing (when tokens.index > 0) + // Skip EOF check when cache hit, as it has already been validated on the first pass. + if (tokens.index() > 0 && tokens.LA(1) != Token.EOF) { + throw new AlertExpressionException(bundle.getString("alerter.calculate.parse.error")); + } + AlertExpressionEvalVisitor visitor = new AlertExpressionEvalVisitor(executor, tokens); + return visitor.visit(tree); + } + + private CommonTokenStream createTokenStream(String expr) { + AlertExpressionLexer lexer = new AlertExpressionLexer(CharStreams.fromString(expr)); + return new CommonTokenStream(lexer); + } + + @Override + public Map<String, Object> getAvailableExecutors() { + boolean hasPromqlExecutor = false; + boolean hasSqlExecutor = false; + java.util.Set<String> availableExecutors = new java.util.HashSet<>(); + + if (executors != null) { + for (QueryExecutor executor : executors) { + String datasource = executor.getDatasource(); + availableExecutors.add(datasource); + + // Check if executor supports promql + if (executor.support(WarehouseConstants.PROMQL)) { + hasPromqlExecutor = true; + } + // Check if executor supports sql + if (executor.support(WarehouseConstants.SQL)) { + hasSqlExecutor = true; + } + } + } + + Map<String, Object> result = new java.util.HashMap<>(8); + result.put("hasPromqlExecutor", hasPromqlExecutor); + result.put("hasSqlExecutor", hasSqlExecutor); + result.put("availableExecutors", availableExecutors); + + return result; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DefaultExternAlertService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DefaultExternAlertService.java new file mode 100644 index 0000000..cd7d549 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DefaultExternAlertService.java @@ -0,0 +1,86 @@ +/* + * 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.service.impl; + +import java.time.Instant; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.ExternAlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * Default external alarm service impl + */ +@Slf4j +@Service +public class DefaultExternAlertService implements ExternAlertService { + + @Autowired + private AlarmCommonReduce alarmCommonReduce; + + @Override + public void addExternAlert(String content) { + SingleAlert alert = JsonUtil.fromJson(content, SingleAlert.class); + if (alert == null) { + log.warn("parse extern alert content failed! content: {}", content); + throw new IllegalArgumentException("parse extern alert content failed!"); + } + alert.setId(null); + String status = alert.getStatus(); + if (status == null) { + // can use the startAt and endAt to determine the status + if (alert.getStartAt() != null && alert.getEndAt() != null) { + if (alert.getEndAt() < Instant.now().toEpochMilli()) { + status = CommonConstants.ALERT_STATUS_RESOLVED; + } else { + status = CommonConstants.ALERT_STATUS_FIRING; + } + } else { + status = CommonConstants.ALERT_STATUS_FIRING; + } + } + alert.setStatus(status); + if (CommonConstants.ALERT_STATUS_FIRING.equals(status)) { + alert.setEndAt(null); + if (alert.getStartAt() == null) { + alert.setStartAt(Instant.now().toEpochMilli()); + } + if (alert.getActiveAt() == null) { + alert.setActiveAt(Instant.now().toEpochMilli()); + } + } else { + alert.setActiveAt(null); + if (alert.getStartAt() == null) { + alert.setStartAt(Instant.now().toEpochMilli()); + } + if (alert.getEndAt() == null) { + alert.setEndAt(Instant.now().toEpochMilli()); + } + } + alarmCommonReduce.reduceAndSendAlarm(alert); + } + + @Override + public String supportSource() { + return "default"; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/HuaweiCloudExternAlertService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/HuaweiCloudExternAlertService.java new file mode 100644 index 0000000..075e993 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/HuaweiCloudExternAlertService.java @@ -0,0 +1,404 @@ +/* + * 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.service.impl; + +import tools.jackson.databind.JsonNode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.ExternAlertService; +import org.apache.hertzbeat.alert.util.DateUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.support.exception.IgnoreException; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.springframework.stereotype.Service; + +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.Signature; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; +import java.time.Instant; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.NOTIFICATION; +import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.SUBSCRIPTION; +import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.UNSUBSCRIBE; +import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_MESSAGE; +import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_MESSAGE_ID; +import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_SUBJECT; +import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_SUBSCRIBE_URL; +import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TIMESTAMP; +import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TOPIC_URN; +import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TYPE; + +/** + * Huawei cloud external alarm service impl + */ +@Slf4j +@Service +public class HuaweiCloudExternAlertService implements ExternAlertService { + + private static final String CERTIFICATE_TYPE = "X.509"; + + private static final String CHARSET_UTF8 = StandardCharsets.UTF_8.name(); + + private static final String SUBSCRIBE_URL_PREFIX = "https://console.huaweicloud.com/smn/subscription/confirm"; + + private final AlarmCommonReduce alarmCommonReduce; + + public HuaweiCloudExternAlertService(AlarmCommonReduce alarmCommonReduce) { + this.alarmCommonReduce = alarmCommonReduce; + } + + @Override + public void addExternAlert(String content) { + HuaweiCloudExternAlert externAlert = JsonUtil.fromJson(content, HuaweiCloudExternAlert.class); + if (externAlert == null || StringUtils.isBlank(externAlert.getMessage())) { + log.warn("Failure to parse external alert content. content: {}", content); + return; + } + if (!isMessageValid(externAlert)) { + log.warn("Huawei cloud alert verify failed. content: {}", content); + return; + } + process(externAlert); + } + + /** + * Process according to different types + * + * @param externAlert alert content entity + */ + private void process(HuaweiCloudExternAlert externAlert) { + if (NOTIFICATION.getType().equals(externAlert.getType())) { + Optional.ofNullable(buildSendAlert(externAlert)).ifPresent(alarmCommonReduce::reduceAndSendAlarm); + } else if (SUBSCRIPTION.getType().equals(externAlert.getType())) { + autoSubscribeForUrl(externAlert.getSubscribeUrl()); + } else if (UNSUBSCRIBE.getType().equals(externAlert.getType())) { + log.warn("Huawei cloud notifies the recipient of the notification to cancel the subscription."); + } + } + + /** + * Build single alert. + * + * @param externAlert alert content entity + * @return single alert + */ + private SingleAlert buildSendAlert(HuaweiCloudExternAlert externAlert) { + HuaweiCloudExternAlert.AlertMessage message = JsonUtil.fromJson(externAlert.getMessage(), HuaweiCloudExternAlert.AlertMessage.class); + if (null == message || null == message.getData()) { + log.warn("Failure to parse external alert message. message: {}", externAlert.getMessage()); + return null; + } + // Note: Empty and false are both recovery notifications. + // Note: There are no recovery notifications for event types + boolean isAlarm = null != message.getData().getAlarm() && message.getData().getAlarm(); + Long alarmTime = DateUtil.getZonedTimeStampFromFormat(message.getData().getAlarmTime(), "yyyy/MM/dd HH:mm:ss 'GMT'XXX"); + return SingleAlert.builder() + .triggerTimes(1) + .status(isAlarm ? CommonConstants.ALERT_STATUS_FIRING : CommonConstants.ALERT_STATUS_RESOLVED) + .startAt(alarmTime) + .activeAt(Instant.now().toEpochMilli()) + .endAt(isAlarm ? null : alarmTime) + .labels(buildLabels(message.getData())) + .annotations(buildAnnotations(message.getData())) + .content(formatContent(externAlert.getSubject(), message.getData())) + .build(); + } + + /** + * Build basic annotations and fill annotations for huawei cloud. + * + * @param alertData alert content entity + * @return annotations + */ + private Map<String, String> buildAnnotations(HuaweiCloudExternAlert.AlertData alertData) { + Map<String, String> annotations = new HashMap<>(8); + if (null != alertData) { + putIfNotBlank(annotations, "region", alertData.getRegion()); + putIfNotBlank(annotations, "dimensionName", alertData.getDimensionName()); + putIfNotBlank(annotations, "resourceName", alertData.getResourceName()); + putIfNotBlank(annotations, "alarmRecordId", alertData.getAlarmRecordId()); + } + return annotations; + } + + /** + * Build basic labels and fill labels for huawei cloud. + * + * @param alertData alert content entity + * @return labels + */ + private Map<String, String> buildLabels(HuaweiCloudExternAlert.AlertData alertData) { + Map<String, String> labels = new HashMap<>(8); + labels.put("__source__", "huaweicloud-ces"); + if (null != alertData) { + putIfNotBlank(labels, "namespace", alertData.getNamespace()); + putIfNotBlank(labels, "metricName", alertData.getMetricName()); + putIfNotBlank(labels, "resourceId", alertData.getResourceId()); + putIfNotBlank(labels, "level", alertData.getAlarmLevel()); + } + return labels; + } + + /** + * todo i18n + * + * @param subject alert subject + * @param alertData alert content entity + * @return content + */ + private String formatContent(String subject, HuaweiCloudExternAlert.AlertData alertData) { + if (null == alertData) { + return subject; + } + return MessageFormat.format( + "{0} threshold:{1}{2}, current:{3}", + subject, + alertData.getComparisonOperator(), + alertData.getValue(), + alertData.getCurrentData() + ); + } + + /** + * Automatic subscription url. + * + * @param subscribeUrl subscribeUrl + */ + public void autoSubscribeForUrl(String subscribeUrl) { + if (StringUtils.isBlank(subscribeUrl)) { + return; + } + if (!subscribeUrl.startsWith(SUBSCRIBE_URL_PREFIX)) { + throw new SecurityException("Untrusted domain: " + subscribeUrl); + } + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpGet httpGet = new HttpGet(subscribeUrl); + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + + if (statusCode != 200) { + log.error("Subscribe url request failed with status code: " + statusCode + ", response: " + responseBody); + return; + } + JsonNode jsonResponse = JsonUtil.fromJson(responseBody); + if (jsonResponse == null) { + throw new IgnoreException("Subscribe url failed with status code: " + statusCode + ", response: " + responseBody); + } + JsonNode surnNode = jsonResponse.get("subscription_urn"); + if (surnNode == null || StringUtils.isBlank(surnNode.asText())) { + throw new IgnoreException("Subscribe url failed with status code: " + statusCode + ", response: " + responseBody); + } + log.info("Successfully subscribed to Huawei Cloud(SMN) url."); + } + } catch (Exception e) { + log.error("Failed to subscribe url request: {}", e.getMessage()); + } + } + + /** + * Verifying the signature of huawei cloud alert message. + * + * @param externAlert alert content entity + * @return verification result + * @throws SecurityException thrown when validation fails + */ + private boolean isMessageValid(HuaweiCloudExternAlert externAlert) { + try { + String signMessage = buildSignMessage(externAlert); + if (StringUtils.isBlank(signMessage)) { + throw new SecurityException("Verify sign message is null"); + } + X509Certificate cert = getCertificate(externAlert.getSigningCertUrl()); + return verifySignature(signMessage, cert, externAlert.getSignature()); + } catch (Exception e) { + log.error("Failed to verify message signature: ", e); + return false; + } + } + + /** + * Build sign message. + * + * @param externAlert alert content entity + * @return sign message + */ + private String buildSignMessage(HuaweiCloudExternAlert externAlert) { + if (NOTIFICATION.getType().equals(externAlert.getType())) { + return buildNotificationMessage(externAlert); + } else if (SUBSCRIPTION.getType().equals(externAlert.getType()) || UNSUBSCRIBE.getType().equals(externAlert.getType())){ + return buildSubscriptionMessage(externAlert); + } + return null; + } + + /** + * Building sign message of 'Notification' type + * + * @param externAlert alert content entity + * @return sign message + */ + private String buildNotificationMessage(HuaweiCloudExternAlert externAlert) { + StringBuilder message = new StringBuilder(); + appendField(message, FIELD_MESSAGE, externAlert.getMessage()); + appendField(message, FIELD_MESSAGE_ID, externAlert.getMessageId()); + if (StringUtils.isNotBlank(externAlert.getSubject())) { + appendField(message, FIELD_SUBJECT, externAlert.getSubject()); + } + appendField(message, FIELD_TIMESTAMP, externAlert.getTimestamp()); + appendField(message, FIELD_TOPIC_URN, externAlert.getTopicUrn()); + appendField(message, FIELD_TYPE, externAlert.getType()); + return message.toString(); + } + + /** + * Building sign message of 'SubscriptionConfirmation' or 'UnsubscribeConfirmation' type + * + * @param externAlert alert content entity + * @return sign message + */ + private String buildSubscriptionMessage(HuaweiCloudExternAlert externAlert) { + StringBuilder message = new StringBuilder(); + appendField(message, FIELD_MESSAGE, externAlert.getMessage()); + appendField(message, FIELD_MESSAGE_ID, externAlert.getMessageId()); + appendField(message, FIELD_SUBSCRIBE_URL, externAlert.getSubscribeUrl()); + appendField(message, FIELD_TIMESTAMP, externAlert.getTimestamp()); + appendField(message, FIELD_TOPIC_URN, externAlert.getTopicUrn()); + appendField(message, FIELD_TYPE, externAlert.getType()); + return message.toString(); + } + + /** + * Obtain certificate + * + * @param signCertUrl sign cert url + * @return X509 certificate + * @throws Exception Thrown when certificate acquisition fails + */ + private X509Certificate getCertificate(String signCertUrl) throws Exception { + URL url = new URL(signCertUrl); + if (!"https".equalsIgnoreCase(url.getProtocol())) { + throw new SecurityException("Only HTTPS is allowed"); + } + boolean trusted = Arrays.stream(Region.values()).anyMatch(ep -> ep.getEndpoint().equals(url.getHost())); + if (!trusted) { + throw new SecurityException("Untrusted domain: " + url.getHost()); + } + try (InputStream in = url.openStream()) { + CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE); + return (X509Certificate) cf.generateCertificate(in); + } + } + + /** + * Verify signature + * + * @param message sign message + * @param cert cert + * @param signature signature + * @return verification result + * @throws Exception thrown when an error occurs in the validation process + */ + private boolean verifySignature(String message, X509Certificate cert, String signature) throws Exception { + Signature sig = Signature.getInstance(cert.getSigAlgName()); + sig.initVerify(cert.getPublicKey()); + sig.update(message.getBytes(CHARSET_UTF8)); + return sig.verify(Base64.getDecoder().decode(signature)); + } + + private void putIfNotBlank(Map<String, String> map, String key, String value) { + if (StringUtils.isNotBlank(value)){ + map.put(key, value); + } + } + + private void appendField(StringBuilder builder, String fieldName, String value) { + builder.append(fieldName).append("\n").append(value).append("\n"); + } + + @Override + public String supportSource() { + return "huaweicloud-ces"; + } + + + /** + * doc: <a href="https://console.huaweicloud.com/apiexplorer/#/endpoint/SMN">SMN API</a> + */ + @Getter + private enum Region { + + AE_AD_1("ae-ad-1", "smn.ae-ad-1.myhuaweicloud.com"), + AF_SOUTH_1("af-south-1", "smn.af-south-1.myhuaweicloud.com"), + AP_SOUTHEAST_1("ap-southeast-1", "smn.ap-southeast-1.myhuaweicloud.com"), + AP_SOUTHEAST_2("ap-southeast-2", "smn.ap-southeast-2.myhuaweicloud.com"), + AP_SOUTHEAST_3("ap-southeast-3", "smn.ap-southeast-3.myhuaweicloud.com"), + AP_SOUTHEAST_4("ap-southeast-4", "smn.ap-southeast-4.myhuaweicloud.com"), + CN_EAST_2("cn-east-2", "smn.cn-east-2.myhuaweicloud.com"), + CN_EAST_3("cn-east-3", "smn.cn-east-3.myhuaweicloud.com"), + CN_EAST_4("cn-east-4", "smn.cn-east-4.myhuaweicloud.com"), + CN_EAST_5("cn-east-5", "smn.cn-east-5.myhuaweicloud.com"), + CN_NORTH_1("cn-north-1", "smn.cn-north-1.myhuaweicloud.com"), + CN_NORTH_11("cn-north-11", "smn.cn-north-11.myhuaweicloud.com"), + CN_NORTH_12("cn-north-12", "smn.cn-north-12.myhuaweicloud.com"), + CN_NORTH_2("cn-north-2", "smn.cn-north-2.myhuaweicloud.cn"), + CN_NORTH_4("cn-north-4", "smn.cn-north-4.myhuaweicloud.com"), + CN_NORTH_9("cn-north-9", "smn.cn-north-9.myhuaweicloud.com"), + CN_SOUTH_1("cn-south-1", "smn.cn-south-1.myhuaweicloud.com"), + CN_SOUTH_2("cn-south-2", "smn.cn-south-2.myhuaweicloud.com"), + CN_SOUTH_4("cn-south-4", "smn.cn-south-4.myhuaweicloud.com"), + CN_SOUTHWEST_2("cn-southwest-2", "smn.cn-southwest-2.myhuaweicloud.com"), + CN_SOUTHWEST_3("cn-southwest-3", "smn.cn-southwest-3.myhuaweicloud.com"), + EU_WEST_0("eu-west-0", "smn.eu-west-0.myhuaweicloud.com"), + LA_NORTH_2("la-north-2", "smn.la-north-2.myhuaweicloud.com"), + LA_SOUTH_2("la-south-2", "smn.la-south-2.myhuaweicloud.com"), + MY_KUALALUMPUR_1("my-kualalumpur-1", "smn.my-kualalumpur-1.myhuaweicloud.com"), + NA_MEXICO_1("na-mexico-1", "smn.na-mexico-1.myhuaweicloud.com"), + RU_MOSCOW_1("ru-moscow-1", "smn.ru-moscow-1.myhuaweicloud.com"), + SA_BRAZIL_1("sa-brazil-1", "smn.sa-brazil-1.myhuaweicloud.com"), + TR_WEST_1("tr-west-1", "smn.tr-west-1.myhuaweicloud.com"), + EU_WEST_101("eu-west-101", "smn.eu-west-101.myhuaweicloud.eu"); + + private final String id; + + private final String endpoint; + + Region(String id, String endpoint) { + this.id = id; + this.endpoint = endpoint; + } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java new file mode 100644 index 0000000..dc4521a --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java @@ -0,0 +1,388 @@ +/* + * 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.service.impl; + +import jakarta.persistence.criteria.Predicate; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.cache.CacheFactory; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +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.notice.AlertNoticeDispatch; +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.common.entity.alerter.SingleAlert; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Message notification configuration implementation + */ +@Service +@Order(value = Ordered.HIGHEST_PRECEDENCE) +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class NoticeConfigServiceImpl implements NoticeConfigService, CommandLineRunner { + + private static final Map<Byte, NoticeTemplate> PRESET_TEMPLATE = new HashMap<>(16); + + @Autowired + private NoticeReceiverDao noticeReceiverDao; + + @Autowired + private NoticeRuleDao noticeRuleDao; + + @Autowired + private NoticeTemplateDao noticeTemplateDao; + + @Autowired + @Lazy + private AlertNoticeDispatch dispatcherAlarm; + + @Override + public Page<NoticeReceiver> getNoticeReceivers(String name, int pageIndex, int pageSize) { + Specification<NoticeReceiver> specification = (root, query, criteriaBuilder) -> { + Predicate predicate = criteriaBuilder.conjunction(); + if (StringUtils.isNotBlank(name)) { + Predicate predicateName = criteriaBuilder.like( + criteriaBuilder.lower(root.get("name")), "%" + name.toLowerCase() + "%" + ); + predicate = criteriaBuilder.and(predicateName); + } + return predicate; + }; + return noticeReceiverDao.findAll(specification, PageRequest.of(pageIndex, pageSize, Sort.by(Sort.Direction.DESC, "id"))); + } + + @Override + public List<NoticeReceiver> getAllNoticeReceivers() { + return noticeReceiverDao.findAll(); + } + + @Override + public Page<NoticeTemplate> getNoticeTemplates(String name, boolean preset, int pageIndex, int pageSize) { + if (preset) { + // Query preset templates + List<NoticeTemplate> defaultTemplates = new LinkedList<>(PRESET_TEMPLATE.values()); + + // Filter by name (case-insensitive) + List<NoticeTemplate> filteredDefaultTemplates = defaultTemplates.stream() + .filter(template -> StringUtils.isBlank(name) + || template.getName().toLowerCase().contains(name.toLowerCase())) + .collect(Collectors.toList()); + + // Pagination logic + int totalItems = filteredDefaultTemplates.size(); + int fromIndex = Math.min(pageIndex * pageSize, totalItems); + int toIndex = Math.min(fromIndex + pageSize, totalItems); + + if (fromIndex >= totalItems) { + return new PageImpl<>(Collections.emptyList(), PageRequest.of(pageIndex, pageSize), totalItems); + } + + List<NoticeTemplate> paginatedTemplates = filteredDefaultTemplates.subList(fromIndex, toIndex); + return new PageImpl<>(paginatedTemplates, PageRequest.of(pageIndex, pageSize), totalItems); + } else { + // Query custom templates + Specification<NoticeTemplate> specification = (root, query, criteriaBuilder) -> { + Predicate predicate = criteriaBuilder.conjunction(); + if (StringUtils.isNotBlank(name)) { + Predicate predicateName = criteriaBuilder.like( + criteriaBuilder.lower(root.get("name")), "%" + name.toLowerCase() + "%" + ); + predicate = criteriaBuilder.and(predicateName); + } + return predicate; + }; + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, Sort.by(Sort.Direction.DESC, "id")); + return noticeTemplateDao.findAll(specification, pageRequest); + } + } + + + + @Override + public List<NoticeTemplate> getAllNoticeTemplates() { + List<NoticeTemplate> defaultTemplates = new LinkedList<>(PRESET_TEMPLATE.values()); + defaultTemplates.addAll(noticeTemplateDao.findAll()); + return defaultTemplates; + } + + @Override + public Page<NoticeRule> getNoticeRules(String name, int pageIndex, int pageSize) { + Specification<NoticeRule> specification = (root, query, criteriaBuilder) -> { + Predicate predicate = criteriaBuilder.conjunction(); + if (StringUtils.isNotBlank(name)) { + Predicate predicateName = criteriaBuilder.like( + criteriaBuilder.lower(root.get("name")), "%" + name.toLowerCase() + "%" + ); + predicate = criteriaBuilder.and(predicateName); + } + return predicate; + }; + return noticeRuleDao.findAll(specification, PageRequest.of(pageIndex, pageSize, Sort.by(Sort.Direction.DESC, "id"))); + } + + @Override + public void addReceiver(NoticeReceiver noticeReceiver) { + noticeReceiverDao.save(noticeReceiver); + } + + @Override + public void editReceiver(NoticeReceiver noticeReceiver) { + noticeReceiverDao.save(noticeReceiver); + } + + @Override + public void deleteReceiver(Long receiverId) { + noticeReceiverDao.deleteById(receiverId); + } + + @Override + public void addNoticeRule(NoticeRule noticeRule) { + noticeRuleDao.save(noticeRule); + clearNoticeRulesCache(); + } + + @Override + public void editNoticeRule(NoticeRule noticeRule) { + noticeRuleDao.save(noticeRule); + clearNoticeRulesCache(); + } + + @Override + public void deleteNoticeRule(Long ruleId) { + noticeRuleDao.deleteById(ruleId); + clearNoticeRulesCache(); + } + + @Override + public List<NoticeRule> getReceiverFilterRule(GroupAlert alert) { + // use cache + List<NoticeRule> rules = CacheFactory.getNoticeCache(); + if (rules == null) { + rules = noticeRuleDao.findNoticeRulesByEnableTrue(); + CacheFactory.setNoticeCache(rules); + } + + // The temporary rule is to forward all, and then implement more matching rules: alarm status selection, monitoring type selection, etc. + // TODO: This matches an already-grouped alert against notice rules (group-then-route). It cannot fully + // separate alerts that were grouped together but should reach different receivers, so a rule matched by + // one alert still notifies the whole group. The ideal design is route-then-group (like Alertmanager): + // route each single alert by its labels first, then group per receiver. Tracked as a follow-up to #3852. + return rules.stream() + .filter(rule -> { + if (!rule.isFilterAll()) { + // filter labels: a rule matches when ANY single alert in the group carries + if (rule.getLabels() != null && !rule.getLabels().isEmpty()) { + List<SingleAlert> singleAlerts = alert.getAlerts(); + boolean labelMatch = singleAlerts != null && singleAlerts.stream().anyMatch(singleAlert -> { + Map<String, String> alertLabels = singleAlert.getLabels(); + if (alertLabels == null) { + return false; + } + return rule.getLabels().entrySet().stream().allMatch(labelItem -> + Objects.equals(labelItem.getValue(), alertLabels.get(labelItem.getKey()))); + }); + if (!labelMatch) { + return false; + } + } + } + + LocalDateTime nowDate = LocalDateTime.now(); + // filter day + int currentDayOfWeek = nowDate.toLocalDate().getDayOfWeek().getValue(); + if (rule.getDays() != null && !rule.getDays().isEmpty()) { + boolean dayMatch = rule.getDays().stream().anyMatch(item -> item == currentDayOfWeek); + if (!dayMatch) { + return false; + } + } + // filter time + LocalTime nowTime = nowDate.toLocalTime(); + boolean startMatch = rule.getPeriodStart() == null + || nowTime.isAfter(rule.getPeriodStart().toLocalTime()) + || (rule.getPeriodEnd() != null && rule.getPeriodStart().isAfter(rule.getPeriodEnd()) + && nowTime.isBefore(rule.getPeriodStart().toLocalTime())); + boolean endMatch = rule.getPeriodEnd() == null + || nowTime.isBefore(rule.getPeriodEnd().toLocalTime()); + return startMatch && endMatch; + }) + .collect(Collectors.toList()); + } + + @Override + public NoticeTemplate getOneTemplateById(Long id) { + return noticeTemplateDao.findById(id).orElse(null); + } + + + @Override + public NoticeReceiver getReceiverById(Long receiverId) { + return noticeReceiverDao.findById(receiverId).orElse(null); + } + + @Override + public NoticeRule getNoticeRulesById(Long ruleId) { + return noticeRuleDao.findById(ruleId).orElse(null); + } + + + @Override + public void addNoticeTemplate(NoticeTemplate noticeTemplate) { + noticeTemplateDao.save(noticeTemplate); + clearNoticeRulesCache(); + } + + @Override + public void editNoticeTemplate(NoticeTemplate noticeTemplate) { + noticeTemplateDao.save(noticeTemplate); + clearNoticeRulesCache(); + } + + @Override + public void deleteNoticeTemplate(Long templateId) { + noticeTemplateDao.deleteById(templateId); + clearNoticeRulesCache(); + } + + @Override + public Optional<NoticeTemplate> getNoticeTemplatesById(Long templateId) { + return noticeTemplateDao.findById(templateId); + } + + @Override + public NoticeTemplate getDefaultNoticeTemplateByType(Byte type) { + if (type == null) { + return null; + } + return PRESET_TEMPLATE.get(type); + } + + @Override + public boolean sendTestMsg(NoticeReceiver 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"); + Map<String, String> annotations = new HashMap<>(8); + annotations.put("suggest", "Please check the CPU usage of the server"); + SingleAlert singleAlert1 = SingleAlert.builder() + .labels(labels) + .content("test send msg! \\n This is the test data. It is proved that it can be received successfully") + .startAt(System.currentTimeMillis()) + .activeAt(System.currentTimeMillis()) + .endAt(System.currentTimeMillis()) + .triggerTimes(2) + .annotations(annotations) + .status("firing") + .build(); + SingleAlert singleAlert2 = SingleAlert.builder() + .labels(labels) + .content("test send msg! \\n This is the test data. It is proved that it can be received successfully") + .startAt(System.currentTimeMillis()) + .activeAt(System.currentTimeMillis()) + .endAt(System.currentTimeMillis()) + .triggerTimes(4) + .annotations(annotations) + .status("firing") + .build(); + GroupAlert groupAlert = GroupAlert.builder() + .commonLabels(Map.of(CommonConstants.LABEL_ALERT_NAME, "CPU Usage Alert")) + .commonAnnotations(annotations) + .alerts(List.of(singleAlert1, singleAlert2)) + .status("firing") + .build(); + return dispatcherAlarm.sendNoticeMsg(noticeReceiver, null, groupAlert); + } + + private void clearNoticeRulesCache() { + CacheFactory.clearNoticeCache(); + } + + @Override + public void run(String... args) throws Exception { + try { + log.info("load default notice template in internal jar"); + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resolver.getResources("classpath:templates/*.*"); + for (Resource resource : resources) { + if (resource.getFilename() == null || (!resource.getFilename().endsWith("txt") && !resource.getFilename().endsWith("html"))) { + log.warn("Ignore the template file {}.", resource.getFilename()); + continue; + } + try (InputStream inputStream = resource.getInputStream()) { + byte[] bytes = new byte[inputStream.available()]; + inputStream.read(bytes); + String content = new String(bytes, StandardCharsets.UTF_8); + NoticeTemplate template = new NoticeTemplate(); + String name = resource.getFilename().replace(".txt", "").replace(".html", ""); + String[] names = name.split("-"); + if (names.length != 2) { + log.warn("Ignore the template file {}.", resource.getFilename()); + continue; + } + byte type = Byte.parseByte(names[0]); + name = names[1]; + template.setName(name); + template.setType(type); + template.setPreset(true); + template.setContent(content); + template.setGmtUpdate(LocalDateTime.now()); + PRESET_TEMPLATE.put(template.getType(), template); + } catch (IOException e) { + log.error(e.getMessage(), e); + log.error("Ignore this template file: {}.", resource.getFilename()); + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/PrometheusExternAlertService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/PrometheusExternAlertService.java new file mode 100644 index 0000000..0fbb6fb --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/PrometheusExternAlertService.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service.impl; + +import tools.jackson.core.type.TypeReference; +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dto.PrometheusExternAlert; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.ExternAlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +/** + * Prometheus external alarm service impl + */ +@Slf4j +@Service +public class PrometheusExternAlertService implements ExternAlertService { + + @Autowired + private AlarmCommonReduce alarmCommonReduce; + + + @Override + public void addExternAlert(String content) { + + TypeReference<List<PrometheusExternAlert>> typeReference = new TypeReference<>() {}; + List<PrometheusExternAlert> alerts = JsonUtil.fromJson(content, typeReference); + if (alerts == null || alerts.isEmpty()) { + log.warn("parse prometheus extern alert content failed! content: {}", content); + return; + } + for (PrometheusExternAlert alert : alerts) { + Map<String, String> annotations = alert.getAnnotations(); + if (annotations == null) { + annotations = new HashMap<>(8); + } + if (StringUtils.hasText(alert.getGeneratorURL())) { + annotations.put("generatorURL", alert.getGeneratorURL()); + } + String description = annotations.get("description"); + if (description == null) { + description = annotations.get("summary"); + } + if (description == null) { + description = annotations.values().stream().findFirst().orElse(""); + } + Map<String, String> labels = alert.getLabels(); + if (labels == null) { + labels = new HashMap<>(8); + } + labels.put("__source__", "prometheus"); + String status = CommonConstants.ALERT_STATUS_FIRING; + if (alert.getEndsAt() != null && alert.getEndsAt().isBefore(Instant.now())) { + status = CommonConstants.ALERT_STATUS_RESOLVED; + } + SingleAlert singleAlert = SingleAlert.builder() + .content(description) + .status(status) + .activeAt(CommonConstants.ALERT_STATUS_FIRING.equals(status) ? Instant.now().toEpochMilli() : null) + .startAt(alert.getStartsAt() != null ? alert.getStartsAt().toEpochMilli() : Instant.now().toEpochMilli()) + .endAt(CommonConstants.ALERT_STATUS_RESOLVED.equals(status) ? alert.getEndsAt().toEpochMilli() : null) + .labels(labels) + .annotations(alert.getAnnotations()) + .triggerTimes(1) + .build(); + + alarmCommonReduce.reduceAndSendAlarm(singleAlert); + } + } + + @Override + public String supportSource() { + return "prometheus"; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SkyWalkingExternAlertService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SkyWalkingExternAlertService.java new file mode 100644 index 0000000..889e879 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SkyWalkingExternAlertService.java @@ -0,0 +1,95 @@ +/* + * 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.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dto.SkyWalkingExternAlert; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.ExternAlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import tools.jackson.core.type.TypeReference; + +/** + * SkyWalking external alarm service impl + */ +@Slf4j +@Service +public class SkyWalkingExternAlertService implements ExternAlertService { + + @Autowired + private AlarmCommonReduce alarmCommonReduce; + + + @Override + public void addExternAlert(String content) { + TypeReference<List<SkyWalkingExternAlert>> typeReference = new TypeReference<>() {}; + List<SkyWalkingExternAlert> alerts = JsonUtil.fromJson(content, typeReference); + if (alerts == null || alerts.isEmpty()) { + log.warn("Parse SkyWalking extern alert content failed! content: {}", content); + return; + } + for (SkyWalkingExternAlert alert : alerts) { + SingleAlert singleAlert = SingleAlert.builder() + .content(alert.getAlarmMessage()) + .status(CommonConstants.ALERT_STATUS_FIRING) + .activeAt(Instant.now().toEpochMilli()) + .startAt(alert.getStartTime() != null ? alert.getStartTime() : Instant.now().toEpochMilli()) + .labels(acquireAlertLabels(alert)) + .annotations(acquireAlertAnnotations(alert)) + .triggerTimes(1) + .build(); + alarmCommonReduce.reduceAndSendAlarm(singleAlert); + } + } + + @Override + public String supportSource() { + return "skywalking"; + } + + private Map<String, String> acquireAlertLabels(SkyWalkingExternAlert externAlert){ + Map<String, String> labels = new HashMap<>(8); + labels.put("__source__", "skywalking"); + List<SkyWalkingExternAlert.Tag> tags = externAlert.getTags(); + if (tags == null || tags.isEmpty()){ + return labels; + } + tags.forEach(tag -> labels.put(tag.getKey(), tag.getValue())); + return labels; + } + + private Map<String, String> acquireAlertAnnotations(SkyWalkingExternAlert externAlert){ + Map<String, String> annotations = new HashMap<>(8); + annotations.putIfAbsent("scope", externAlert.getScope()); + annotations.putIfAbsent("name", externAlert.getName()); + annotations.putIfAbsent("id0", externAlert.getId0()); + annotations.putIfAbsent("id1", externAlert.getId1()); + annotations.putIfAbsent("ruleName", externAlert.getRuleName()); + return annotations; + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImpl.java new file mode 100644 index 0000000..bcb16f6 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImpl.java @@ -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.alert.service.impl; + +import tools.jackson.databind.JsonNode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties; +import org.apache.hertzbeat.alert.service.SmsClient; +import org.apache.hertzbeat.common.constants.SmsConstants; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.support.exception.SendMessageException; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +/** + * Smslocal SMS Client Implement + */ +@Slf4j +public class SmsLocalSmsClientImpl implements SmsClient { + private static final String HOST = "secure.smslocal.com"; + + private static final String PATH = "/api/service/enterprise-service/external/sms"; + + private static final String FROM = "Hertzbeat"; + + private static final String SUCCESS_CODE = "200"; + + private final SmslocalSmsProperties config; + + public SmsLocalSmsClientImpl(SmslocalSmsProperties smslocalSmsProperties) { + this.config = smslocalSmsProperties; + } + + @Override + public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + if (Objects.isNull(receiver) || Objects.isNull(alert)) { + log.warn("receiver and alert can not be null! receiver: {}, alert:{}", receiver, alert); + return; + } + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + String content = alert.getCommonAnnotations().get("summary"); + if (Objects.isNull(content) || Objects.isNull(alert.getCommonAnnotations().get("description"))) { + content = alert.getAlerts().get(0).getContent(); + } + SmsMessage smsMessage = new SmsMessage(FROM, receiver.getPhone(), content); + + String payload = JsonUtil.toJson(smsMessage); + + HttpPost httpPost = new HttpPost("https://" + HOST + PATH); + httpPost.setHeader("Content-Type", "application/json; charset=utf-8"); + httpPost.setHeader("Token", config.getApiKey()); + httpPost.setEntity(new StringEntity(payload, StandardCharsets.UTF_8)); + + log.debug("Sending SMS request to {}, payload: {}", httpPost.getURI(), payload); + + // send http request and handle response + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + + log.debug("SMS response status: {}, body: {}", statusCode, responseBody); + + if (statusCode != 200) { + throw new SendMessageException("HTTP request failed with status code: " + statusCode); + } + + JsonNode jsonResponse = JsonUtil.fromJson(responseBody); + JsonNode jsonNode = jsonResponse.get(0); + if (Objects.isNull(jsonNode)) { + log.warn("jsonResponse parse errorCode failed: {}", jsonResponse); + return; + } + String errorCode = jsonNode.get("errorCode").asText(); + if (!SUCCESS_CODE.equals(errorCode)) { + String msgid = jsonNode.get("id").asText(); + throw new SendMessageException(errorCode + ":" + msgid); + } + + log.info("Successfully sent SMS to phone: {}", receiver.getPhone()); + } + } catch (Exception e) { + log.error("Failed to send SMS: {}", e.getMessage()); + throw new SendMessageException(e.getMessage()); + } + + } + + @Override + public String getType() { + return SmsConstants.SMSLOCAL; + } + + @Override + public boolean checkConfig() { + if (Objects.isNull(config) || Objects.isNull(config.getApiKey()) || config.getApiKey().isBlank()) { + log.warn("smslocal properties can not be null: {}", config); + return false; + } + return true; + } + + @Getter + @Setter + private static class SmsMessage { + String from; + String to; + String content; + final int datacoding = 0; + final String direction = "mt"; + + public SmsMessage(String from, String to, String content) { + this.from = from; + this.to = to; + this.content = content; + } + } + +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TencentExternAlertService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TencentExternAlertService.java new file mode 100644 index 0000000..3c3891a --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TencentExternAlertService.java @@ -0,0 +1,227 @@ +/* + * 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.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.dto.TencentCloudExternAlert; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.ExternAlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Default external alarm service impl + */ +@Slf4j +@Service +public class TencentExternAlertService implements ExternAlertService { + + @Autowired + private AlarmCommonReduce alarmCommonReduce; + + @Override + public void addExternAlert(String content) { + TencentCloudExternAlert report = JsonUtil.fromJson(content, TencentCloudExternAlert.class); + if (report == null) { + log.warn("parse extern alert content failed! content: {}", content); + return; + } + SingleAlert alert = new TencentCloudAlertConverter().convert(report); + alarmCommonReduce.reduceAndSendAlarm(alert); + } + + @Override + public String supportSource() { + return "tencent"; + } + + /** + * Converter: TencentCloud alert to SingleAlert + */ + public static class TencentCloudAlertConverter { + + /** + * Metric alert content template + */ + private final String metricTemplate = "Tencent Cloud Metric Alert: {metricShowName} {calcType} {calcValue}{calcUnit}, Current: {currentValue}{unit}"; + + /** + * Event alert content template + */ + private final String eventTemplate = "Tencent Cloud Event Alert: {productShowName} - {eventShowName}"; + + + /** + * Convert TencentCloud alert to SingleAlert + */ + public SingleAlert convert(TencentCloudExternAlert tencentAlert) { + // Building basic information + SingleAlert.SingleAlertBuilder builder = SingleAlert.builder() + .status(convertStatus(tencentAlert.getAlarmStatus())) + .startAt(parseTime(tencentAlert.getFirstOccurTime())) + .activeAt(parseTime(tencentAlert.getFirstOccurTime())) + .triggerTimes(1); + // If it is a recovery state, set the end time. + if ("0".equals(tencentAlert.getAlarmStatus())) { + builder.endAt(parseTime(tencentAlert.getRecoverTime())); + } + + // Build labels + Map<String, String> labels = new HashMap<>(); + buildLabels(labels, tencentAlert); + + // Build annotations + Map<String, String> annotations = new HashMap<>(); + buildAnnotations(annotations, tencentAlert); + + // Build content + String content = generateContent(tencentAlert); + + return builder + .labels(labels) + .annotations(annotations) + .content(content) + .build(); + } + + private void buildLabels(Map<String, String> labels, TencentCloudExternAlert alert) { + labels.put("__source__", "tencent_cloud"); + labels.put("alert_type", alert.getAlarmType()); + if (TencentCloudExternAlert.METRIC.equals(alert.getAlarmType())) { + labels.put("metric_name", alert.getAlarmPolicyInfo().getConditions().getMetricName()); + labels.put("namespace", alert.getAlarmObjInfo().getNamespace()); + } else { + labels.put("event_name", alert.getAlarmPolicyInfo().getConditions().getEventName()); + labels.put("product_name", alert.getAlarmPolicyInfo().getConditions().getProductName()); + } + } + + private void buildAnnotations(Map<String, String> annotations, TencentCloudExternAlert alert) { + TencentCloudExternAlert.AlarmPolicyInfo alarmPolicyInfo = alert.getAlarmPolicyInfo(); + TencentCloudExternAlert.AlarmObjInfo alarmObjInfo = alert.getAlarmObjInfo(); + TencentCloudExternAlert.Dimensions dimensions = alert.getAlarmObjInfo().getDimensions(); + + putIfNotNull(annotations, "policy_id", alarmPolicyInfo.getPolicyId()); + putIfNotNull(annotations, "policy_type", alarmPolicyInfo.getPolicyType()); + putIfNotNull(annotations, "policy_name", alarmPolicyInfo.getPolicyName()); + putIfNotNull(annotations, "policy_type_cname", alarmPolicyInfo.getPolicyTypeCname()); + + putIfNotNull(annotations, "namespace", alarmObjInfo.getNamespace()); + putIfNotNull(annotations, "region", alarmObjInfo.getRegion()); + putIfNotNull(annotations, "app_id", alarmObjInfo.getAppId()); + putIfNotNull(annotations, "uin", alarmObjInfo.getUin()); + + putIfNotNull(annotations, "instance_id", dimensions.getUnInstanceId()); + putIfNotNull(annotations, "obj_id", dimensions.getObjId()); + + } + + /** + * Generate alert content with template + */ + private String generateContent(TencentCloudExternAlert alert) { + if (TencentCloudExternAlert.METRIC.equals(alert.getAlarmType())) { + return generateMetricContent(alert); + } else { + return generateEventContent(alert); + } + } + + /** + * Generate metric alert content + */ + private String generateMetricContent(TencentCloudExternAlert alert) { + TencentCloudExternAlert.Conditions conditions = alert.getAlarmPolicyInfo().getConditions(); + Map<String, String> params = new HashMap<>(); + + params.put("metricShowName", conditions.getMetricShowName()); + + if (StringUtils.isNotEmpty(conditions.getCalcType()) + && StringUtils.isNotEmpty(conditions.getCalcValue())) { + params.put("calcType", conditions.getCalcType()); + params.put("calcValue", conditions.getCalcValue()); + params.put("calcUnit", Objects.toString(conditions.getCalcUnit(), "")); + } else { + params.put("calcType", ""); + params.put("calcValue", "N/A"); + params.put("calcUnit", ""); + } + + if (StringUtils.isNotEmpty(conditions.getCurrentValue())) { + params.put("currentValue", conditions.getCurrentValue()); + params.put("unit", Objects.toString(conditions.getUnit(), "")); + } else { + params.put("currentValue", "N/A"); + params.put("unit", ""); + } + return replacePlaceholders(metricTemplate, params); + } + + /** + * Generate event alert content + */ + private String generateEventContent(TencentCloudExternAlert alert) { + TencentCloudExternAlert.Conditions conditions = alert.getAlarmPolicyInfo().getConditions(); + Map<String, String> params = new HashMap<>(); + params.put("productShowName", conditions.getProductShowName()); + params.put("eventShowName", conditions.getEventShowName()); + return replacePlaceholders(eventTemplate, params); + } + + /** + * Replace placeholders in template with actual values + */ + private String replacePlaceholders(String template, Map<String, String> params) { + String result = template; + for (Map.Entry<String, String> entry : params.entrySet()) { + result = result.replace("{" + entry.getKey() + "}", Objects.toString(entry.getValue(), "NULL")); + } + return result; + } + + private String convertStatus(String alarmStatus) { + return "1".equals(alarmStatus) ? CommonConstants.ALERT_STATUS_FIRING : CommonConstants.ALERT_STATUS_RESOLVED; + } + + private Long parseTime(String timeStr) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return sdf.parse(timeStr).getTime(); + } catch (ParseException e) { + log.error("Failed to parse time: {}", timeStr); + throw new IllegalArgumentException("Failed to parse time: " + timeStr, e); + } + } + + private void putIfNotNull(Map<String, String> map, String key, String value){ + if (StringUtils.isNotEmpty(value)){ + map.put(key, value); + } + } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TencentSmsClientImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TencentSmsClientImpl.java new file mode 100644 index 0000000..90ceefe --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TencentSmsClientImpl.java @@ -0,0 +1,223 @@ +/* + * 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.service.impl; + +import jakarta.xml.bind.DatatypeConverter; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.sms.TencentSmsProperties; +import org.apache.hertzbeat.alert.service.SmsClient; +import org.apache.hertzbeat.alert.util.CryptoUtils; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.support.exception.SendMessageException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.apache.hertzbeat.common.util.JsonUtil; +import tools.jackson.databind.JsonNode; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +import static org.apache.hertzbeat.common.constants.SmsConstants.TENCENT; + +/** + * sms service client for tencent cloud <br> + * doc: <a href="https://cloud.tencent.com/document/api/382/55981">https://cloud.tencent.com/document/api/382/55981</a> + */ + +@Slf4j +public class TencentSmsClientImpl implements SmsClient { + + private static final String RESPONSE_OK = "Ok"; + private static final String REGION = "ap-guangzhou"; + private static final String API_VERSION = "2021-01-11"; + private static final String ACTION = "SendSms"; + private static final String HOST = "sms.tencentcloudapi.com"; + private static final Charset UTF8 = StandardCharsets.UTF_8; + + private String appId; + private String signName; + private String templateId; + private String secretId; + private String secretKey; + + public TencentSmsClientImpl(TencentSmsProperties config) { + if (config != null) { + this.appId = config.getAppId(); + this.signName = config.getSignName(); + this.templateId = config.getTemplateId(); + this.secretId = config.getSecretId(); + this.secretKey = config.getSecretKey(); + } + } + + @Override + public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + // todo limit the number of words + String instance = null; + String priority = null; + String content = null; + if (alert.getCommonLabels() != null) { + instance = alert.getCommonLabels().get("instance"); + priority = alert.getCommonLabels().get("priority"); + content = alert.getCommonAnnotations().get("summary"); + content = content == null ? alert.getCommonAnnotations().get("description") : content; + if (content == null) { + content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null); + } + } + + String[] templateValues = new String[3]; + templateValues[0] = instance == null ? alert.getGroupKey() : instance; + templateValues[1] = priority == null ? "unknown" : priority; + templateValues[2] = content; + + String[] phones = new String[1]; + phones[0] = receiver.getPhone(); + + sendSms(this.appId, this.signName, this.templateId, templateValues, phones); + } + + public void sendSms(String appId, String signName, String templateId, + String[] templateValues, String[] phones) { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + + // build request payload + Map<String, Object> params = new HashMap<>(); + params.put("SmsSdkAppId", appId); + params.put("SignName", signName); + params.put("TemplateId", templateId); + params.put("TemplateParamSet", templateValues); + params.put("PhoneNumberSet", phones); + + String payload = JsonUtil.toJson(params); + + // calculate request signature + String authorization = calculateAuthorization( + secretId, secretKey, "sms", HOST, REGION, + ACTION, API_VERSION, payload); + + // build http request + HttpPost httpPost = new HttpPost("https://" + HOST); + httpPost.setHeader("Content-Type", "application/json; charset=utf-8"); + httpPost.setHeader("Host", HOST); + httpPost.setHeader("X-TC-Action", ACTION); + httpPost.setHeader("X-TC-Timestamp", timestamp); + httpPost.setHeader("X-TC-Version", API_VERSION); + httpPost.setHeader("X-TC-Region", REGION); + httpPost.setHeader("Authorization", authorization); + httpPost.setEntity(new StringEntity(payload, StandardCharsets.UTF_8)); + + log.debug("Sending SMS request to {}, payload: {}", httpPost.getURI(), payload); + + // send http request and handle response + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + + log.debug("SMS response status: {}, body: {}", statusCode, responseBody); + + if (statusCode != 200) { + throw new SendMessageException("HTTP request failed with status code: " + statusCode); + } + + JsonNode jsonResponse = JsonUtil.fromJson(responseBody); + JsonNode responseNode = jsonResponse.get("Response"); + JsonNode error = responseNode.get("Error"); + if (error != null) { + String code = error.get("Code").asText(); + String message = error.get("Message").asText(); + throw new SendMessageException(code + ":" + message); + } + JsonNode sendStatusSet = responseNode.get("SendStatusSet"); + if (sendStatusSet != null && sendStatusSet.isArray() && sendStatusSet.size() > 0) { + JsonNode firstStatus = sendStatusSet.get(0); + String code = firstStatus.get("Code").asText(); + String message = firstStatus.get("Message").asText(); + if (!RESPONSE_OK.equals(code)) { + throw new SendMessageException(code + ":" + message); + } + } + log.info("Successfully sent SMS to phones: {}", String.join(",", phones)); + } + } catch (Exception e) { + log.warn("Failed to send SMS: {}", e.getMessage()); + throw new SendMessageException(e.getMessage()); + } + } + + @Override + public String getType() { + return TENCENT; + } + + @Override + public boolean checkConfig() { + if (appId.isBlank() || templateId.isBlank() || secretId.isBlank() || secretKey.isBlank()) { + return false; + } + return true; + } + + public static String calculateAuthorization(String secretId, String secretKey, + String service, String host, String region, + String action, String version, String payload) throws Exception { + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + String date = sdf.format(new Date(Long.valueOf(timestamp + "000"))); + + // Step 1: Construct the canonical request string + String httpRequestMethod = "POST"; + String canonicalUri = "/"; + String canonicalQueryString = ""; + String canonicalHeaders = "content-type:application/json; charset=utf-8\n" + "host:" + host + "\n"; + String signedHeaders = "content-type;host"; + String hashedRequestPayload = CryptoUtils.sha256Hex(payload); + String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload; + + // Step 2: Construct the string to sign + String algorithm = "TC3-HMAC-SHA256"; + String credentialScope = date + "/" + service + "/" + "tc3_request"; + String hashedCanonicalRequest = CryptoUtils.sha256Hex(canonicalRequest); + String stringToSign = algorithm + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest; + + // Step 3: Calculate the signature + byte[] secretDate = CryptoUtils.hmac256(("TC3" + secretKey).getBytes(UTF8), date); + byte[] secretService = CryptoUtils.hmac256(secretDate, service); + byte[] secretSigning = CryptoUtils.hmac256(secretService, "tc3_request"); + String signature = DatatypeConverter.printHexBinary( + CryptoUtils.hmac256(secretSigning, stringToSign)).toLowerCase(); + + // Step 4: Construct the Authorization header + return algorithm + " " + "Credential=" + secretId + "/" + credentialScope + ", " + + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TwilioSmsClientImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TwilioSmsClientImpl.java new file mode 100644 index 0000000..55e1fd3 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TwilioSmsClientImpl.java @@ -0,0 +1,171 @@ +/* + * 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.service.impl; + +import tools.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.sms.TwilioSmsProperties; +import org.apache.hertzbeat.alert.service.SmsClient; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.support.exception.SendMessageException; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.apache.http.client.entity.UrlEncodedFormEntity; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +import static org.apache.hertzbeat.common.constants.SmsConstants.TWILIO; + +/** + * Twilio SMS Client Implementation<br> + * API doc: <a href= + * "https://www.twilio.com/docs/sms/api">https://www.twilio.com/docs/sms/api</a> + */ +@Slf4j +public class TwilioSmsClientImpl implements SmsClient { + + private static final String API_URL_FORMAT = "https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json"; + private static final String MESSAGE_TEMPLATE = "Instance: %s, Priority: %s, Content: %s"; + + private final String accountSid; + private final String authToken; + private final String twilioPhoneNumber; + + public TwilioSmsClientImpl(TwilioSmsProperties config) { + if (config != null) { + this.accountSid = config.getAccountSid(); + this.authToken = config.getAuthToken(); + this.twilioPhoneNumber = config.getTwilioPhoneNumber(); + } else { + this.accountSid = ""; + this.authToken = ""; + this.twilioPhoneNumber = ""; + } + } + + @Override + public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + String instance = null; + String priority = null; + String content = null; + if (alert.getCommonLabels() != null) { + instance = alert.getCommonLabels().get("instance") == null ? alert.getGroupKey() + : alert.getCommonLabels().get("instance"); + priority = alert.getCommonLabels().get("priority") == null ? "unknown" + : alert.getCommonLabels().get("priority"); + content = alert.getCommonAnnotations().get("summary"); + content = content == null ? alert.getCommonAnnotations().get("description") : content; + if (content == null) { + content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null); + } + } + this.send(receiver.getPhone(), createMessage(instance, priority, content)); + } + + private String createMessage(String instance, String priority, String content) { + return String.format(MESSAGE_TEMPLATE, instance, priority, content); + } + + private void send(String phoneNumber, String message) { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + String endpoint = String.format(API_URL_FORMAT, accountSid); + URI requestUri = new URI(endpoint); + + HttpPost httpPost = createHttpPost(requestUri, phoneNumber, message); + log.info("Sending Twilio SMS request to {}", requestUri); + executeRequest(httpClient, httpPost, phoneNumber); + } catch (Exception e) { + log.warn("Failed to send SMS: {}", e.getMessage()); + throw new SendMessageException(e.getMessage()); + } + } + + private HttpPost createHttpPost(URI requestUri, String toNumber, String message) { + HttpPost httpPost = new HttpPost(requestUri); + + String auth = accountSid + ":" + authToken; + String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes()); + httpPost.setHeader("Authorization", "Basic " + encodedAuth); + + List<BasicNameValuePair> parameters = new ArrayList<>(); + parameters.add(new BasicNameValuePair("To", toNumber)); + parameters.add(new BasicNameValuePair("From", twilioPhoneNumber)); + parameters.add(new BasicNameValuePair("Body", message)); + + try { + httpPost.setEntity(new UrlEncodedFormEntity(parameters)); + return httpPost; + } catch (Exception e) { + log.error("Failed to create HTTP request: {}", e.getMessage()); + throw new SendMessageException(e.getMessage()); + } + } + + private void executeRequest(CloseableHttpClient httpClient, HttpPost httpPost, String phoneNumber) + throws Exception { + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + log.info("SMS response status: {}, body: {}", statusCode, responseBody); + + if (statusCode < 200 || statusCode >= 300) { + + if (responseBody.contains("21608")) { + throw new SendMessageException( + "The Twilio trial account can only send SMS to verified phone numbers"); + } else { + throw new SendMessageException( + "HTTP request failed with status code: " + statusCode + ", response: " + responseBody); + } + } + + JsonNode jsonResponse = JsonUtil.fromJson(responseBody); + if (jsonResponse == null) { + throw new SendMessageException(statusCode + ":" + responseBody); + } + + JsonNode sidNode = jsonResponse.get("sid"); + if (sidNode == null) { + throw new SendMessageException(statusCode + ":" + responseBody); + } + + String sid = sidNode.asText(); + log.info("Successfully sent SMS to phone: {}, sid: {}", phoneNumber, sid); + } + } + + @Override + public String getType() { + return TWILIO; + } + + @Override + public boolean checkConfig() { + return !(accountSid.isBlank() || authToken.isBlank() || twilioPhoneNumber.isBlank()); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/UniSmsClientImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/UniSmsClientImpl.java new file mode 100644 index 0000000..d4a414e --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/UniSmsClientImpl.java @@ -0,0 +1,193 @@ +/* + * 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.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.sms.UniSmsProperties; +import org.apache.hertzbeat.alert.service.SmsClient; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.support.exception.SendMessageException; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.apache.hertzbeat.alert.util.CryptoUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.stream.Collectors; +import tools.jackson.databind.JsonNode; + +import static org.apache.hertzbeat.common.constants.SmsConstants.UNISMS; + +/** + * UniSMS client implementation <br/> + * doc:<a href="https://unisms.apistd.com/docs/api/send">https://unisms.apistd.com/docs/api/send</a> + */ +@Slf4j +public class UniSmsClientImpl implements SmsClient { + + private static final String API_URL = "https://uni.apistd.com"; + private static final String ACTION = "sms.message.send"; + private static final String SUCCESS_CODE = "0"; + private static final String HMAC_ALGORITHM = "hmac-sha256"; + + private final UniSmsProperties config; + + public UniSmsClientImpl(UniSmsProperties config) { + this.config = config; + } + + @Override + public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + // build request parameters + Map<String, Object> params = new HashMap<>(); + params.put("to", receiver.getPhone()); + params.put("signature", config.getSignature()); + params.put("templateId", config.getTemplateId()); + + // build template data + Map<String, String> templateData = new HashMap<>(); + String instance = alert.getCommonLabels().getOrDefault("instance", alert.getGroupKey()); + String priority = alert.getCommonLabels().getOrDefault("priority", "unknown"); + String content = alert.getCommonAnnotations().get("summary"); + content = content == null ? alert.getCommonAnnotations().get("description") : content; + if (content == null) { + content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null); + } + + templateData.put("instance", instance); + templateData.put("priority", priority); + templateData.put("content", content); + params.put("templateData", templateData); + + // build URL and request headers + String url; + if ("hmac".equalsIgnoreCase(config.getAuthMode())) { + url = buildHmacUrl(); + } else { + url = buildSimpleUrl(); + } + + // send HTTP request + HttpPost httpPost = new HttpPost(url); + httpPost.setHeader("Content-Type", "application/json"); + httpPost.setHeader("Accept", "application/json"); + + String payload = JsonUtil.toJson(params); + httpPost.setEntity(new StringEntity(payload, StandardCharsets.UTF_8)); + + log.info("Sending SMS request to UniSMS, payload: {}, url: {}", payload, url); + + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + handleResponse(response, receiver.getPhone()); + } + } catch (Exception e) { + log.error("Failed to send SMS via UniSMS: {}", e.getMessage()); + throw new SendMessageException(e.getMessage()); + } + } + + private String buildSimpleUrl() { + return String.format("%s/?action=%s&accessKeyId=%s", + API_URL, ACTION, config.getAccessKeyId()); + } + + private String buildHmacUrl() { + long timestamp = System.currentTimeMillis(); + String nonce = generateNonce(); + + // build query parameters + Map<String, String> params = new TreeMap<>(); + params.put("accessKeyId", config.getAccessKeyId()); + params.put("action", ACTION); + params.put("algorithm", HMAC_ALGORITHM); + params.put("nonce", nonce); + params.put("timestamp", String.valueOf(timestamp)); + + // build sign text + String signText = params.entrySet().stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("&")); + + // calculate signature + String signature = CryptoUtils.hmacSha256Base64(config.getAccessKeySecret(), signText); + return String.format("%s/?action=%s&accessKeyId=%s&algorithm=%s×tamp=%d&nonce=%s&signature=%s", + API_URL, ACTION, config.getAccessKeyId(), HMAC_ALGORITHM, timestamp, nonce, signature); + } + + private String generateNonce() { + return UUID.randomUUID().toString().replace("-", "").substring(0, 16); + } + + private void handleResponse(CloseableHttpResponse response, String phone) throws IOException { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + + log.info("UniSMS response status: {}, body: {}", statusCode, responseBody); + + if (statusCode != 200) { + throw new SendMessageException("HTTP request failed with status code: " + statusCode + ", response: " + responseBody); + } + + JsonNode jsonResponse = JsonUtil.fromJson(responseBody); + String code = jsonResponse.get("code").asText(); + if (!SUCCESS_CODE.equals(code)) { + String message = jsonResponse.get("message").asText(); + throw new SendMessageException(code + ":" + message); + } + + log.info("Successfully sent SMS to phone: {}", phone); + } + + @Override + public String getType() { + return UNISMS; + } + + @Override + public boolean checkConfig() { + if (config == null + || config.getAccessKeyId() == null + || config.getAccessKeyId().isBlank() + || config.getSignature() == null + || config.getSignature().isBlank() + || config.getTemplateId() == null + || config.getTemplateId().isBlank()) { + return false; + } + + // HMAC mode requires additional check for accessKeySecret + if ("hmac".equalsIgnoreCase(config.getAuthMode()) + && (config.getAccessKeySecret() == null || config.getAccessKeySecret().isBlank())) { + return false; + } + + return true; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/UptimeKumaExternAlertServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/UptimeKumaExternAlertServiceImpl.java new file mode 100644 index 0000000..27d8789 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/UptimeKumaExternAlertServiceImpl.java @@ -0,0 +1,118 @@ +/* + * 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.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dto.UptimeKumaExternAlert; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.ExternAlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; + +/** + * uptime-kuma external alarm service impl + */ +@Slf4j +@Service +public class UptimeKumaExternAlertServiceImpl implements ExternAlertService { + + @Autowired + private AlarmCommonReduce alarmCommonReduce; + + @Override + public void addExternAlert(String content) { + UptimeKumaExternAlert alert = JsonUtil.fromJson(content, UptimeKumaExternAlert.class); + if (alert == null) { + log.warn("parse extern alert content failed! content: {}", content); + return; + } + SingleAlert singleAlert = new UptimeKumaAlertConverter().convert(alert); + alarmCommonReduce.reduceAndSendAlarm(singleAlert); + } + + /** + * Converter: UptimeKuma alert to SingleAlert + */ + public static class UptimeKumaAlertConverter { + + /** + * Convert UptimeKuma alert to SingleAlert + */ + public SingleAlert convert(UptimeKumaExternAlert alert) { + // build basic info + SingleAlert.SingleAlertBuilder builder = SingleAlert.builder() + .status(convertStatus(alert.getHeartbeat().getStatus())) + .startAt(parseTime(alert.getHeartbeat().getTime())) + .activeAt(parseTime(alert.getHeartbeat().getTime())) + .triggerTimes(1); + + // build labels + Map<String, String> labels = new HashMap<>(); + labels.put("__source__", "uptime_kuma"); + labels.put("monitor_id", String.valueOf(alert.getMonitor().getId())); + labels.put("monitor_name", alert.getMonitor().getName()); + + // build annotations + Map<String, String> annotations = new HashMap<>(); + annotations.put("description", alert.getMonitor().getDescription()); + annotations.put("message", alert.getHeartbeat().getMsg()); + annotations.put("important", String.valueOf(alert.getHeartbeat().isImportant())); + + return builder + .labels(labels) + .annotations(annotations) + .content(buildContent(alert)) + .build(); + } + + private String buildContent(UptimeKumaExternAlert alert) { + return String.format("Monitor [%s] %s: %s", + alert.getMonitor().getName(), + alert.getMonitor().getDescription(), + alert.getHeartbeat().getMsg()); + } + + private String convertStatus(int status) { + // uptime kuma status: 1-up, 0-down, 2-pending + return status == 1 ? CommonConstants.ALERT_STATUS_RESOLVED : CommonConstants.ALERT_STATUS_FIRING; + } + + private Long parseTime(String timeStr) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return sdf.parse(timeStr).getTime(); + } catch (ParseException e) { + log.error("Failed to parse time: {}", timeStr); + throw new IllegalArgumentException("Failed to parse time: " + timeStr, e); + } + } + } + + @Override + public String supportSource() { + return "uptime-kuma"; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/VolcEngineExternAlertService.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/VolcEngineExternAlertService.java new file mode 100644 index 0000000..65a282e --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/VolcEngineExternAlertService.java @@ -0,0 +1,244 @@ +/* + * 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.service.impl; + +import tools.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.hertzbeat.alert.dto.VolcEngineExternEventAlert; +import org.apache.hertzbeat.alert.dto.VolcEngineExternMetricAlert; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.ExternAlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.stereotype.Service; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Volcengine alarm entity class + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VolcEngineExternAlertService implements ExternAlertService { + + private final AlarmCommonReduce alarmCommonReduce; + private static final Map<String, Integer> severityOrder = Map.of( + CommonConstants.ALERT_SEVERITY_CRITICAL, 1, + CommonConstants.ALERT_SEVERITY_WARNING, 2, + CommonConstants.ALERT_SEVERITY_INFO, 3 + ); + + @Override + public void addExternAlert(String content) { + JsonNode root = JsonUtil.fromJson(content); + if (root == null) { + log.warn("parse extern alert content failed! content: {}", content); + return; + } + String type = root.get("Type").asText(); + if (VolcEngineExternMetricAlert.ALERT_TYPE_EVENT.equals(type)) { + VolcEngineExternEventAlert eventAlert = JsonUtil.fromJson(content, VolcEngineExternEventAlert.class); + if (eventAlert == null) { + log.warn("parse extern event alert content failed! content: {}", content); + return; + } + SingleAlert singleAlert = new VolcEngineAlertConverter().convertEventToSingleAlert(eventAlert); + alarmCommonReduce.reduceAndSendAlarm(singleAlert); + + } else { + // deal with metric alert + VolcEngineExternMetricAlert report = JsonUtil.fromJson(content, VolcEngineExternMetricAlert.class); + if (report == null) { + log.warn("parse extern metrics alert content failed! content: {}", content); + return; + } + for (SingleAlert singleAlert : new VolcEngineAlertConverter().convertMetricAlertToSingeAlert(report)) { + alarmCommonReduce.reduceAndSendAlarm(singleAlert); + } + } + } + + @Override + public String supportSource() { + return "volcengine"; + } + + /** + * Converter: VolcEngine alert to SingleAlert + */ + public static class VolcEngineAlertConverter { + + /** + * Convert VolcEngine metric alert to SingleAlert List + */ + public List<SingleAlert> convertMetricAlertToSingeAlert(VolcEngineExternMetricAlert alert) { + String status = convertStatus(alert); + if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_METRIC)) { + return convertMetricAlert(alert, status, alert.getResources()); + } + if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_METRICS_NODATA)) { + return convertMetricAlert(alert, status, alert.getNoDataResources()); + } + if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_NO_DATA_RECOVERED)) { + return convertMetricAlert(alert, status, alert.getNoDataRecoveredResources()); + } + if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_METRIC_RECOVERED)) { + return convertMetricAlert(alert, status, alert.getRecoveredResources()); + } + return List.of(); + } + + public SingleAlert convertEventToSingleAlert(VolcEngineExternEventAlert alert) { + SingleAlert.SingleAlertBuilder builder = SingleAlert.builder() + .status(CommonConstants.ALERT_STATUS_FIRING) + .startAt(alert.getHappenedAt() * 1000) + .activeAt(alert.getHappenedAt() * 1000) + .labels(buildEventLabels(alert)) + .content(alert.getDescriptionCn()) + .annotations(new HashMap<>()); + return builder.build(); + } + + /** + * build labels for volcengine event alert + */ + private Map<String, String> buildEventLabels(VolcEngineExternEventAlert event) { + Map<String, String> labels = new HashMap<>(); + labels.put("severity", convertEventSeverity(event)); + labels.put("__source__", "volcengine"); + labels.put("resource_name", event.getDetails().getVolcResourceName()); + labels.put("account_id", event.getAccountId()); + labels.put("region", event.getRegion()); + labels.put("event_type", event.getEventType()); + labels.put("source", event.getSource()); + return labels; + + } + + /** + * convert volcengine event alert status to heartbeat alert status + * use the most severe level in the rules + * + * @param alert volcengine event alert + * @return status + */ + private String convertEventSeverity(VolcEngineExternEventAlert alert) { + return alert.getRules().stream().map(VolcEngineExternEventAlert.EventRule::getLevel) + .min((o1, o2) -> severityOrder.get(convertCommonSeverity(o1)).compareTo(severityOrder.get(convertCommonSeverity(o2)))) + .orElse(CommonConstants.ALERT_SEVERITY_INFO); + } + + private String convertCommonSeverity(String level) { + return switch (level) { + case "critical" -> CommonConstants.ALERT_SEVERITY_CRITICAL; + case "warning" -> CommonConstants.ALERT_SEVERITY_WARNING; + default -> CommonConstants.ALERT_SEVERITY_INFO; + }; + } + + /** + * create SingleAlert for each resources in volcengine alert + * + * @param alert volcengine alert + * @param status status + * @param resources resources + * @return List of SingleAlert + */ + private List<SingleAlert> convertMetricAlert(VolcEngineExternMetricAlert alert, String status, + List<? extends VolcEngineExternMetricAlert.Resource> resources) { + if (CollectionUtils.isEmpty(resources)) { + return List.of(); + } + List<SingleAlert> result = new ArrayList<>(); + for (VolcEngineExternMetricAlert.Resource resource : resources) { + SingleAlert.SingleAlertBuilder builder = SingleAlert.builder() + .status(status) + .startAt(resource.getFirstAlertTime() * 1000) + .endAt(resource.getLastAlertTime() * 1000) + .labels(buildLabels(alert, resource)) + .activeAt(convertHappenAt(alert.getHappenedAt())) + .content(resource.getName() + alert.getRuleCondition()) + .annotations(buildAnnotations(resource)); + result.add(builder.build()); + } + return result; + } + + private Long convertHappenAt(String happenAt) { + String cleanedStr = happenAt.replace("UTC", "").replace("(", "").replace(")", ""); + DateTimeFormatter altFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXXX"); + OffsetDateTime odt = OffsetDateTime.parse(cleanedStr, altFormatter); + return odt.toEpochSecond() * 1000; + } + + private Map<String, String> buildLabels(VolcEngineExternMetricAlert alert, VolcEngineExternMetricAlert.Resource resource) { + Map<String, String> labels = new HashMap<>(); + labels.put("__source__", "volcengine"); + labels.put("resource_name", resource.getName()); + labels.put("resource_id", resource.getId()); + labels.put("rule_id", alert.getRuleId()); + return labels; + } + + private Map<String, String> buildAnnotations(VolcEngineExternMetricAlert.Resource resource) { + Map<String, String> annotations = new HashMap<>(); + if (resource instanceof VolcEngineExternMetricAlert.NoDataResource noDataResource) { + for (VolcEngineExternMetricAlert.Metric noDataMetric : noDataResource.getNoDataMetrics()) { + annotations.put(noDataMetric.getName(), "N/A"); + } + } + for (VolcEngineExternMetricAlert.Metric metric : resource.getMetrics()) { + annotations.put(metric.getName(), metric.getCurrentValue() + metric.getUnit()); + } + annotations.put("region", resource.getRegion()); + List<VolcEngineExternMetricAlert.Dimension> dimensions = resource.getDimensions(); + if (CollectionUtils.isNotEmpty(dimensions)) { + for (VolcEngineExternMetricAlert.Dimension dimension : dimensions) { + annotations.put(dimension.getNameCn(), dimension.getValue()); + } + } + return annotations; + } + + /** + * convert volcengine alert status to heartbeat alert status + * + * @param alert volcengine alert + * @return status + */ + private String convertStatus(VolcEngineExternMetricAlert alert) { + String type = alert.getType(); + if (Objects.equals(type, VolcEngineExternMetricAlert.ALERT_TYPE_METRIC) + || Objects.equals(type, VolcEngineExternMetricAlert.ALERT_TYPE_EVENT) + || Objects.equals(type, VolcEngineExternMetricAlert.ALERT_TYPE_METRICS_NODATA)) { + return CommonConstants.ALERT_STATUS_FIRING; + } + return CommonConstants.ALERT_STATUS_RESOLVED; + } + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/ZabbixExternAlertServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/ZabbixExternAlertServiceImpl.java new file mode 100644 index 0000000..76deddf --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/ZabbixExternAlertServiceImpl.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.ExternAlertService; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * zabbix external alarm service impl + */ +@Slf4j +@Service +public class ZabbixExternAlertServiceImpl implements ExternAlertService { + + @Autowired + private AlarmCommonReduce alarmCommonReduce; + + @Override + public void addExternAlert(String content) { + SingleAlert alert = JsonUtil.fromJson(content, SingleAlert.class); + if (alert == null) { + log.warn("parse extern alert content failed! content: {}", content); + return; + } + alarmCommonReduce.reduceAndSendAlarm(alert); + } + + @Override + public String supportSource() { + return "zabbix"; + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertTemplateUtil.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertTemplateUtil.java new file mode 100644 index 0000000..491fe8e --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertTemplateUtil.java @@ -0,0 +1,180 @@ +/* + * 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.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +/** + * Alarm template keyword matching replacement engine tool + */ +@Slf4j +public final class AlertTemplateUtil { + + private AlertTemplateUtil() { + } + + /** + * Match the variable ${key} or ${key.property.subproperty} + * eg: Alert, the instance: ${instance} metrics: ${metrics} is over flow. + * eg: Log alert: ${log.attributes.level} - ${log.message} + */ + private static final Pattern PATTERN = Pattern.compile("\\$\\{([\\w.]+)}"); + + public static String render(String template, Map<String, Object> replaceData) { + if (!StringUtils.hasText(template)) { + return template; + } + if (replaceData == null) { + log.warn("The render replace data is null."); + return template; + } + try { + Matcher matcher = PATTERN.matcher(template); + StringBuilder builder = new StringBuilder(); + while (matcher.find()) { + String propertyPath = matcher.group(1); + Object objectValue = getNestedProperty(replaceData, propertyPath); + String value = objectValue != null ? objectValue.toString() : "NullValue"; + matcher.appendReplacement(builder, Matcher.quoteReplacement(value)); + } + matcher.appendTail(builder); + return builder.toString(); + } catch (Exception e) { + log.error(e.getMessage(), e); + return template; + } + } + + /** + * Get nested property value from object using property path like "log.attributes.demo" + * @param replaceData the root data map + * @param propertyPath the property path, e.g., "log.attributes.demo" + * @return the property value or null if not found + */ + private static Object getNestedProperty(Map<String, Object> replaceData, String propertyPath) { + if (propertyPath == null || propertyPath.isEmpty()) { + return null; + } + + String[] parts = propertyPath.split("\\."); + Object current = replaceData.get(parts[0]); + + if (current == null) { + return null; + } + + // If only one part, return the direct value + if (parts.length == 1) { + return current; + } + + // Navigate through the property path + for (int i = 1; i < parts.length; i++) { + current = getPropertyValue(current, parts[i]); + if (current == null) { + return null; + } + } + + return current; + } + + /** + * Get property value from an object using reflection + * @param obj the object + * @param propertyName the property name + * @return the property value or null if not found + */ + private static Object getPropertyValue(Object obj, String propertyName) { + if (obj == null || propertyName == null) { + return null; + } + + try { + // Handle Map objects + if (obj instanceof Map) { + return ((Map<?, ?>) obj).get(propertyName); + } + + Class<?> clazz = obj.getClass(); + + // Try getter method first (getPropertyName or isPropertyName for boolean) + String getterName = "get" + capitalize(propertyName); + String booleanGetterName = "is" + capitalize(propertyName); + + try { + Method getter = clazz.getMethod(getterName); + return getter.invoke(obj); + } catch (NoSuchMethodException e) { + // Try boolean getter + try { + Method booleanGetter = clazz.getMethod(booleanGetterName); + return booleanGetter.invoke(obj); + } catch (NoSuchMethodException ignored) { + // Continue to field access + } + } + + // Try direct field access + try { + Field field = clazz.getDeclaredField(propertyName); + field.setAccessible(true); + return field.get(obj); + } catch (NoSuchFieldException ignored) { + // Field not found + } + + // Try parent classes + Class<?> superClass = clazz.getSuperclass(); + while (superClass != null && !superClass.equals(Object.class)) { + try { + Field field = superClass.getDeclaredField(propertyName); + field.setAccessible(true); + return field.get(obj); + } catch (NoSuchFieldException ignored) { + // Continue with parent class + } + superClass = superClass.getSuperclass(); + } + + } catch (Exception e) { + log.debug("Failed to access property '{}' on object of type {}: {}", + propertyName, obj.getClass().getSimpleName(), e.getMessage()); + } + + return null; + } + + /** + * Capitalize the first letter of a string + * @param str the input string + * @return the capitalized string + */ + private static String capitalize(String str) { + if (str == null || str.isEmpty()) { + return str; + } + return str.substring(0, 1).toUpperCase() + str.substring(1); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java new file mode 100644 index 0000000..a1ed180 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java @@ -0,0 +1,40 @@ +/* + * 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.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * alert util + */ +public class AlertUtil { + + /** + * calculate fingerprint + * @param fingerPrints finger prints + */ + public static String calculateFingerprint(Map<String, String> fingerPrints) { + List<String> keyList = fingerPrints.keySet().stream().filter(Objects::nonNull).sorted().toList(); + List<String> valueList = fingerPrints.values().stream().filter(Objects::nonNull).sorted().toList(); + return Arrays.hashCode(keyList.toArray(new String[0])) + "-" + + Arrays.hashCode(valueList.toArray(new String[0])); + } +} diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/CryptoUtils.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/CryptoUtils.java new file mode 100644 index 0000000..17de042 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/CryptoUtils.java @@ -0,0 +1,118 @@ +/* + * 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 lombok.extern.slf4j.Slf4j; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Base64; + +/** + * General encryption utility class + */ +@Slf4j +public class CryptoUtils { + + private static final Charset UTF8 = StandardCharsets.UTF_8; + + private CryptoUtils() {} + + /** + * Calculate HMAC-SHA256 signature + * @param key secret key + * @param msg message to be signed + * @return signed byte array + */ + public static byte[] hmac256(byte[] key, String msg) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm()); + mac.init(secretKeySpec); + return mac.doFinal(msg.getBytes(UTF8)); + } catch (Exception e) { + log.error("Failed to calculate HMAC-SHA256: {}", e.getMessage()); + throw new RuntimeException("Failed to calculate HMAC-SHA256", e); + } + } + + /** + * Calculate SHA256 hash and convert to lowercase hexadecimal string + * @param data data to be hashed + * @return lowercase hexadecimal string + */ + public static String sha256Hex(String data) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(data.getBytes(UTF8)); + return bytesToHex(digest).toLowerCase(); + } catch (Exception e) { + log.error("Failed to calculate SHA256: {}", e.getMessage()); + throw new RuntimeException("Failed to calculate SHA256", e); + } + } + + /** + * Calculate HMAC-SHA256 signature and convert to Base64 string + * @param key secret key + * @param data data to be signed + * @return Base64 encoded signature string + */ + public static String hmacSha256Base64(String key, String data) { + byte[] hmacResult = hmac256(key.getBytes(UTF8), data); + return Base64.getEncoder().encodeToString(hmacResult); + } + + /** + * Calculate HMAC-SHA256 signature and convert to lowercase hexadecimal string + * @param key secret key + * @param data data to be signed + * @return lowercase hexadecimal string + */ + public static String hmacSha256Hex(String key, String data) { + return hmacSha256Hex(key.getBytes(UTF8), data); + } + + /** + * Calculate HMAC-SHA256 signature and convert to lowercase hexadecimal string + * Note: This is necessary as, converting a String to byte[] and back may yield different results due to encoding differences or data loss. + * @param key secret key in byte array + * @param data data to be signed + * @return lowercase hexadecimal string + */ + public static String hmacSha256Hex(byte[] key, String data) { + byte[] hmacResult = hmac256(key, data); + return bytesToHex(hmacResult).toLowerCase(); + } + + /** + * Convert byte array to hexadecimal string + * @param bytes byte array to convert + * @return hexadecimal string representation + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte byteValue : bytes) { + result.append(String.format("%02x", byteValue)); + } + return result.toString(); + } +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/DateUtil.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/DateUtil.java new file mode 100644 index 0000000..13c3573 --- /dev/null +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/DateUtil.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.util; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; + +/** + * date time common util + */ +@Slf4j +public final class DateUtil { + + private DateUtil() { + } + + private static final String[] DATE_FORMATS = { + "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'", + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "yyyy-MM-dd HH:mm:ss" + }; + + /** + * convert date to timestamp + * @param date date + * @return timestamp + */ + public static Optional<Long> getTimeStampFromSomeFormats(String date) { + for (String dateFormat : DATE_FORMATS) { + try { + DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder() + .appendPattern(dateFormat) + // enable string conversion in strict mode. + .parseStrict() + .toFormatter(); + LocalDateTime time = LocalDateTime.parse(date, dateTimeFormatter); + return Optional.of(time.toInstant(ZoneOffset.UTC).toEpochMilli()); + } catch (Exception e) { + log.warn("Error parsing date '{}' with format '{}': {}", + date, dateFormat, e.getMessage()); + } + } + + log.error("Error parsing date '{}', no corresponding date format", date); + return Optional.empty(); + } + + /** + * convert format data to timestamp + */ + public static Optional<Long> getTimeStampFromFormat(String date, String format) { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format); + LocalDateTime dateTime = LocalDateTime.parse(date, formatter); + return Optional.of(dateTime.toInstant(java.time.ZoneOffset.UTC).toEpochMilli()); + } catch (Exception e) { + log.error("Error parsing date '{}' with format '{}': {}", + date, format, e.getMessage()); + } + + return Optional.empty(); + } + + /** + * convert format data to timestamp + */ + public static Long getZonedTimeStampFromFormat(String dateStr, String format) { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format); + // the parsed zoned date-time, not null + return ZonedDateTime.parse(dateStr, formatter).toInstant().toEpochMilli(); + } catch (Exception e) { + log.error("Error parsing date '{}' with format '{}': {}", + dateStr, format, e.getMessage()); + } + return null; + } + +} diff --git a/hertzbeat-alerter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hertzbeat-alerter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..bbff659 --- /dev/null +++ b/hertzbeat-alerter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,16 @@ +# 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. + +org.apache.hertzbeat.alert.config.AlerterAutoConfiguration diff --git a/hertzbeat-alerter/src/main/resources/alerter_en_US.properties b/hertzbeat-alerter/src/main/resources/alerter_en_US.properties new file mode 100644 index 0000000..6c30e1f --- /dev/null +++ b/hertzbeat-alerter/src/main/resources/alerter_en_US.properties @@ -0,0 +1,38 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +alerter.availability.recover = Availability Alert Resolved, Monitor Status Normal Now +alerter.availability.collector.recover = Collector Availability Alert Resolved, The collector is online +alerter.availability.collector.offline = Collector Availability Alert Notify, The collector is offline +alerter.alarm.recover = Alert Resolved Notice +alerter.notify.title = HertzBeat Alert Notify +alerter.notify.target = Monitor Target +alerter.notify.monitorId = Monitor ID +alerter.notify.monitorName = Monitor Name +alerter.notify.monitorHost = Monitor Host +alerter.notify.priority = Alert Priority +alerter.notify.triggerTime = Alert Trigger Time +alerter.notify.restoreTime = Alert Restore Time +alerter.notify.times = Alert Trigger Times +alerter.notify.tags = Alert Labels +alerter.notify.content = Alert Content +alerter.notify.console = Console Login +alerter.priority.0 = Emergency Alert +alerter.priority.1 = Critical Alert +alerter.priority.2 = Warning Alert +alerter.calculate.parse.error = Expression is not fully parsed, may have syntax errors or incomplete inputs +alerter.datasource.executor.not.found = No query executor found +alerter.converge.duplicate.name = Group convergence strategy name already exists +alerter.converge.empty.name = Group convergence strategy name cannot be empty diff --git a/hertzbeat-alerter/src/main/resources/alerter_zh_CN.properties b/hertzbeat-alerter/src/main/resources/alerter_zh_CN.properties new file mode 100644 index 0000000..3e307a3 --- /dev/null +++ b/hertzbeat-alerter/src/main/resources/alerter_zh_CN.properties @@ -0,0 +1,38 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +alerter.availability.recover = 可用性告警恢复通知, 任务状态已恢复正常 +alerter.availability.collector.recover = 采集器可用性恢复通知,采集器已上线 +alerter.availability.collector.offline = 采集器可用性告警通知,采集器已下线 +alerter.alarm.recover = 告警恢复通知 +alerter.notify.title = HertzBeat告警通知 +alerter.notify.target = 告警目标对象 +alerter.notify.monitorId = 所属监控任务ID +alerter.notify.monitorName = 所属任务名称 +alerter.notify.monitorHost = 所属任务主机 +alerter.notify.priority = 告警级别 +alerter.notify.triggerTime = 告警触发时间 +alerter.notify.restoreTime = 告警恢复时间 +alerter.notify.times = 告警触发次数 +alerter.notify.tags = 告警标签 +alerter.notify.content = 内容详情 +alerter.notify.console = 登入控制台 +alerter.priority.0 = 紧急告警 +alerter.priority.1 = 严重告警 +alerter.priority.2 = 警告告警 +alerter.calculate.parse.error = 表达式未完全解析,可能存在语法错误或输入不完整 +alerter.datasource.executor.not.found = 未找到查询执行器 +alerter.converge.duplicate.name = 分组收敛策略名称已存在 +alerter.converge.empty.name = 分组收敛策略名称不能为空 diff --git a/hertzbeat-alerter/src/main/resources/alerter_zh_TW.properties b/hertzbeat-alerter/src/main/resources/alerter_zh_TW.properties new file mode 100644 index 0000000..8e765eb --- /dev/null +++ b/hertzbeat-alerter/src/main/resources/alerter_zh_TW.properties @@ -0,0 +1,38 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +alerter.availability.recover = 可用性警報已解決,監視狀態現在正常 +alerter.alarm.recover = 警報解決通知 +alerter.availability.collector.recover = 采集器可用性恢復通知,采集器已上線 +alerter.availability.collector.offline = 采集器可用性告警通知,采集器已下線 +alerter.notify.title = HertzBeat 警報通知 +alerter.notify.target = 監視目標 +alerter.notify.monitorId = 監視 ID +alerter.notify.monitorName = 監視名稱 +alerter.notify.monitorHost = 監視主機 +alerter.notify.priority = 警報優先級 +alerter.notify.triggerTime = 警報觸發時間 +alerter.notify.restoreTime = 警報恢復時間 +alerter.notify.times = 警報觸發次數 +alerter.notify.tags = 警報標籤 +alerter.notify.content = 警報內容 +alerter.notify.console = 控制台登錄 +alerter.priority.0 = 緊急警報 +alerter.priority.1 = 嚴重警報 +alerter.priority.2 = 警告警報 +alerter.calculate.parse.error = 表達式未完全解析,可能存在語法錯誤或輸入不完整 +alerter.datasource.executor.not.found = 未找到查詢執行器 +alerter.converge.duplicate.name = 分組收斂策略名稱已存在 +alerter.converge.empty.name = 分組收斂策略名稱不能為空 diff --git a/hertzbeat-alerter/src/main/resources/expr/AlertExpression.g4 b/hertzbeat-alerter/src/main/resources/expr/AlertExpression.g4 new file mode 100644 index 0000000..2a5e735 --- /dev/null +++ b/hertzbeat-alerter/src/main/resources/expr/AlertExpression.g4 @@ -0,0 +1,289 @@ +/* + * 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. + */ + +grammar AlertExpression; + +expression + : expr EOF + ; + +expr + : LPAREN expr RPAREN # ParenExpr + | left=expr op=(GE|LE|GT|LT|EQ|NE) BOOL? right=expr # ComparisonExpr + | left=expr AND right=expr # AndExpr + | left=expr UNLESS right=expr # UnlessExpr + | left=expr OR right=expr # OrExpr + | promql # PromqlExpr + | selectSql # SqlExpr + | number # LiteralExpr + | SQL_FUNCTION LPAREN string RPAREN # SqlCallExpr + | PROMQL_FUNCTION LPAREN string RPAREN # PromqlCallExpr + ; + +functionCall + : functionName LPAREN parameterList RPAREN + ; + +parameterList + : parameter (COMMA parameter)* + ; + +parameter + : expr + | STAR + | string + | duration + ; + +number + : NUMBER + | FLOAT + | SCIENTIFIC_NUMBER + ; + +string + : STRING + ; + +duration + : DURATION + ; + +functionName + : COUNT + | AVG + | SUM + | MIN + | MAX + | RATE_FUNCTION + | INCREASE_FUNCTION + | HISTOGRAM_QUANTILE_FUNCTION + | BY_FUNCTION + | WITHOUT_FUNCTION + | GROUP_LEFT_FUNCTION + | GROUP_RIGHT_FUNCTION + | IGNORING_FUNCTION + | ON_FUNCTION + | IDENTIFIER + ; + +// SQL grammar for complex queries +selectSql + : SELECT selectFieldList FROM relList (WHERE conditionList)? + (GROUP BY groupByList)? (HAVING conditionList)? + (ORDER BY orderByList)? (LIMIT limitClause)? + ; + +selectFieldList + : selectField (COMMA selectField)* + ; + +selectField + : functionCall (AS? IDENTIFIER)? + | IDENTIFIER (AS? IDENTIFIER)? + | STAR (AS? IDENTIFIER)? + | IDENTIFIER DOT IDENTIFIER (AS? IDENTIFIER)? + ; + +groupByList + : IDENTIFIER (COMMA IDENTIFIER)* + ; + +orderByList + : orderByField (COMMA orderByField)* + ; + +orderByField + : IDENTIFIER (ASC | DESC)? + | functionCall (ASC | DESC)? + ; + +limitClause + : NUMBER + ; + +relList + : relation (COMMA relation)* + ; + +relation + : IDENTIFIER (AS? IDENTIFIER)? + | LPAREN selectSql RPAREN AS IDENTIFIER + ; + +conditionList + : conditionList AND conditionList + | conditionList OR conditionList + | condition + | LPAREN conditionList RPAREN + ; + +compOp + : EQ | LT | GT | LE | GE | NE | LIKE | NOT LIKE | IN | NOT IN | IS | IS NOT + ; + +condition + : conditionUnit compOp conditionUnit + | LPAREN condition RPAREN + | IDENTIFIER BETWEEN number AND number + ; + +conditionUnit + : number + | string + | IDENTIFIER + | IDENTIFIER DOT IDENTIFIER + | NULL + | LPAREN selectSql RPAREN + | functionCall + ; + +// PromQL query expressions +promql + : metricSelector instantVectorOp? + | aggregationOperator LPAREN promql (BY labelList)? RPAREN + | functionCall + | promql LBRACKET duration RBRACKET + | promql LBRACKET duration COLON duration RBRACKET + | promql OFFSET duration + | IDENTIFIER + ; + +metricSelector + : LBRACE labelMatcherList? RBRACE + ; + +labelMatcherList + : labelMatcherItem (COMMA labelMatcherItem)* + ; + +labelMatcherItem + : IDENTIFIER labelMatcherOp string + ; + +labelMatcherOp + : EQ | NE + ; + +labelList + : LPAREN IDENTIFIER (COMMA IDENTIFIER)* RPAREN + ; + +instantVectorOp + : LBRACKET duration RBRACKET + ; + +aggregationOperator + : SUM | AVG | COUNT | MIN | MAX | STDDEV | STDVAR | TOPK | BOTTOMK | QUANTILE + ; + +// Lexer rules + +// Boolean operators +AND : [Aa][Nn][Dd] ; +OR : [Oo][Rr] ; +UNLESS : [Uu][Nn][Ll][Ee][Ss][Ss] ; +NOT : [Nn][Oo][Tt] ; + +// SQL keywords +SELECT : [Ss][Ee][Ll][Ee][Cc][Tt] ; +FROM : [Ff][Rr][Oo][Mm] ; +WHERE : [Ww][Hh][Ee][Rr][Ee] ; +GROUP : [Gg][Rr][Oo][Uu][Pp] ; +BY : [Bb][Yy] ; +HAVING : [Hh][Aa][Vv][Ii][Nn][Gg] ; +ORDER : [Oo][Rr][Dd][Ee][Rr] ; +LIMIT : [Ll][Ii][Mm][Ii][Tt] ; +OFFSET : [Oo][Ff][Ff][Ss][Ee][Tt] ; +AS : [Aa][Ss] ; +ASC : [Aa][Ss][Cc] ; +DESC : [Dd][Ee][Ss][Cc] ; +IN : [Ii][Nn] ; +IS : [Ii][Ss] ; +NULL : [Nn][Uu][Ll][Ll] ; +LIKE : [Ll][Ii][Kk][Ee] ; +BETWEEN : [Bb][Ee][Tt][Ww][Ee][Ee][Nn] ; +STAR : '*' ; + +// Aggregate functions +COUNT : [Cc][Oo][Uu][Nn][Tt] ; +SUM : [Ss][Uu][Mm] ; +AVG : [Aa][Vv][Gg] ; +MIN : [Mm][Ii][Nn] ; +MAX : [Mm][Aa][Xx] ; +STDDEV : [Ss][Tt][Dd][Dd][Ee][Vv] ; +STDVAR : [Ss][Tt][Dd][Vv][Aa][Rr] ; +VARIANCE: [Vv][Aa][Rr][Ii][Aa][Nn][Cc][Ee] ; + +// PromQL specific functions +RATE_FUNCTION : [Rr][Aa][Tt][Ee] ; +INCREASE_FUNCTION: [Ii][Nn][Cc][Rr][Ee][Aa][Ss][Ee] ; +HISTOGRAM_QUANTILE_FUNCTION: [Hh][Ii][Ss][Tt][Oo][Gg][Rr][Aa][Mm] '_' [Qq][Uu][Aa][Nn][Tt][Ii][Ll][Ee] ; +TOPK : [Tt][Oo][Pp][Kk] ; +BOTTOMK : [Bb][Oo][Tt][Tt][Oo][Mm][Kk] ; +QUANTILE: [Qq][Uu][Aa][Nn][Tt][Ii][Ll][Ee] ; +BY_FUNCTION: [Bb][Yy] ; +WITHOUT_FUNCTION: [Ww][Ii][Tt][Hh][Oo][Uu][Tt] ; +GROUP_LEFT_FUNCTION: [Gg][Rr][Oo][Uu][Pp] '_' [Ll][Ee][Ff][Tt] ; +GROUP_RIGHT_FUNCTION: [Gg][Rr][Oo][Uu][Pp] '_' [Rr][Ii][Gg][Hh][Tt] ; +IGNORING_FUNCTION: [Ii][Gg][Nn][Oo][Rr][Ii][Nn][Gg] ; +ON_FUNCTION: [Oo][Nn] ; + +// Other functions +SQL_FUNCTION: [Ss][Qq][Ll] ; +PROMQL_FUNCTION: [Pp][Rr][Oo][Mm][Qq][Ll] ; + +// Comparison operators +GT : '>' ; +GE : '>=' ; +LT : '<' ; +LE : '<=' ; +EQ : '==' | '=' ; +NE : '!=' ; +BOOL : 'bool'; + +// Delimiters +LPAREN : '(' ; +RPAREN : ')' ; +LBRACE : '{' ; +RBRACE : '}' ; +LBRACKET: '[' ; +RBRACKET: ']' ; +COMMA : ',' ; +DOT : '.' ; +COLON : ':' ; +SEMICOLON: ';' ; + +// number formats +SCIENTIFIC_NUMBER: [0-9]+ ('.' [0-9]+)? [eE] [+-]? [0-9]+ ; +FLOAT : [0-9]+ '.' [0-9]+ ; +NUMBER : [0-9]+ ; + +// Duration literals for PromQL (e.g., 5m, 1h, 30s) +DURATION : [0-9]+ [a-zA-Z]+ ; + +// String literals +STRING : '"' (~["\r\n\\] | '\\' .)* '"' + | '\'' (~['\r\n\\] | '\\' .)* '\'' ; + +// Identifiers and metric names +IDENTIFIER : [a-zA-Z_] [a-zA-Z0-9_=~{}[\]".~-]* ; + +// Whitespace and comments +WS : [ \t\r\n]+ -> channel(HIDDEN) ; +LINE_COMMENT : '//' ~[\r\n]* -> skip ; +BLOCK_COMMENT : '/*' .*? '*/' -> skip ; \ No newline at end of file diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/AlerterWorkerPoolTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/AlerterWorkerPoolTest.java new file mode 100644 index 0000000..ba83880 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/AlerterWorkerPoolTest.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert; + +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.concurrent.CountDownLatch; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.hertzbeat.common.concurrent.AdmissionMode; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link AlerterWorkerPool} + */ +class AlerterWorkerPoolTest { + + private static final int NUMBER_OF_TASKS = 10; + + private AlerterWorkerPool pool; + + @AfterEach + void tearDown() { + if (pool != null) { + pool.destroy(); + } + } + + @Test + void executeJob() throws InterruptedException { + pool = new AlerterWorkerPool(); + AtomicInteger counter = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(NUMBER_OF_TASKS); + + for (int i = 0; i < NUMBER_OF_TASKS; i++) { + pool.executeJob(() -> { + counter.incrementAndGet(); + latch.countDown(); + }); + } + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertEquals(NUMBER_OF_TASKS, counter.get()); + } + + @Test + void executeNotifyRunsOnVirtualThread() throws Exception { + pool = new AlerterWorkerPool(); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + + pool.executeNotify((byte) 1, () -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void executeNotifyRejectsWhenGlobalConcurrencyLimitReached() throws Exception { + VirtualThreadProperties properties = new VirtualThreadProperties( + true, + VirtualThreadProperties.PoolProperties.collectorDefaults(), + VirtualThreadProperties.PoolProperties.commonDefaults(), + VirtualThreadProperties.PoolProperties.managerDefaults(), + new VirtualThreadProperties.AlerterProperties( + new VirtualThreadProperties.PoolProperties(AdmissionMode.LIMIT_AND_REJECT, 1), + 10, + VirtualThreadProperties.QueueProperties.logWorkerDefaults(), + VirtualThreadProperties.QueueProperties.reduceDefaults(), + VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(), + 8), + VirtualThreadProperties.PoolProperties.warehouseDefaults(), + VirtualThreadProperties.AsyncProperties.defaults()); + pool = new AlerterWorkerPool(properties); + + CountDownLatch started = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + pool.executeNotify((byte) 1, () -> { + started.countDown(); + try { + release.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertTrue(started.await(5, TimeUnit.SECONDS)); + + try { + assertThrows(RejectedExecutionException.class, () -> pool.executeNotify((byte) 2, () -> { + })); + } finally { + release.countDown(); + } + } + + @Test + void executeNotifyRejectsWhenChannelLimitReached() throws Exception { + VirtualThreadProperties properties = new VirtualThreadProperties( + true, + VirtualThreadProperties.PoolProperties.collectorDefaults(), + VirtualThreadProperties.PoolProperties.commonDefaults(), + VirtualThreadProperties.PoolProperties.managerDefaults(), + new VirtualThreadProperties.AlerterProperties( + new VirtualThreadProperties.PoolProperties(AdmissionMode.LIMIT_AND_REJECT, 8), + 10, + VirtualThreadProperties.QueueProperties.logWorkerDefaults(), + VirtualThreadProperties.QueueProperties.reduceDefaults(), + VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(), + 1), + VirtualThreadProperties.PoolProperties.warehouseDefaults(), + VirtualThreadProperties.AsyncProperties.defaults()); + pool = new AlerterWorkerPool(properties); + + CountDownLatch started = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + pool.executeNotify((byte) 1, () -> { + started.countDown(); + try { + release.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertTrue(started.await(5, TimeUnit.SECONDS)); + + try { + assertThrows(RejectedExecutionException.class, () -> pool.executeNotify((byte) 1, () -> { + })); + } finally { + release.countDown(); + } + } + + @Test + void executeLogJob() throws InterruptedException { + pool = new AlerterWorkerPool(); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + + pool.executeLogJob(() -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + }); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void executeLogJobRejectsWhenQueueCapacityReached() throws InterruptedException { + VirtualThreadProperties properties = new VirtualThreadProperties( + true, + VirtualThreadProperties.PoolProperties.collectorDefaults(), + VirtualThreadProperties.PoolProperties.commonDefaults(), + VirtualThreadProperties.PoolProperties.managerDefaults(), + new VirtualThreadProperties.AlerterProperties( + VirtualThreadProperties.PoolProperties.alerterNotifyDefaults(), + 10, + new VirtualThreadProperties.QueueProperties(1, 1), + VirtualThreadProperties.QueueProperties.reduceDefaults(), + VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(), + 4), + VirtualThreadProperties.PoolProperties.warehouseDefaults(), + VirtualThreadProperties.AsyncProperties.defaults()); + pool = new AlerterWorkerPool(properties); + + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + pool.executeLogJob(() -> { + firstStarted.countDown(); + try { + releaseFirst.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + pool.executeLogJob(secondStarted::countDown); + assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS)); + try { + assertThrows(RejectedExecutionException.class, () -> pool.executeLogJob(() -> { + })); + } finally { + releaseFirst.countDown(); + } + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManagerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManagerTest.java new file mode 100644 index 0000000..95a43ff --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManagerTest.java @@ -0,0 +1,135 @@ +/* + * 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.calculate; + +import org.apache.hertzbeat.alert.dao.SingleAlertDao; +import org.apache.hertzbeat.alert.util.AlertUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +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 java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.when; + +/** + * alert cache manager test + */ +@ExtendWith(MockitoExtension.class) +public class AlarmCacheManagerTest { + + @Mock + private SingleAlertDao singleAlertDao; + + private AlarmCacheManager alarmCacheManager; + + @BeforeEach + public void setUp() { + Map<String, String> labels = new HashMap<>(); + labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL); + labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(1L)); + SingleAlert alert = new SingleAlert(); + alert.setContent("Alert cache manager test"); + alert.setLabels(labels); + when(singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING)).thenReturn(Collections.singletonList(alert)); + alarmCacheManager = new AlarmCacheManager(singleAlertDao); + } + + @Test + void testInit() { + Map<String, String> labels = new HashMap<>(); + labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL); + labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(1L)); + + String fingerprint = AlertUtil.calculateFingerprint(labels); + SingleAlert firingSingleAlert = alarmCacheManager.getFiring(1L, fingerprint); + assertNotNull(firingSingleAlert); + assertEquals("Alert cache manager test", firingSingleAlert.getContent()); + alarmCacheManager.removeFiring(1L, fingerprint); + firingSingleAlert = alarmCacheManager.getFiring(1L, fingerprint); + assertNull(firingSingleAlert); + } + + @Test + void testPending() { + Map<String, String> labels = new HashMap<>(); + labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL); + labels.put(CommonConstants.ALERT_SEVERITY_INFO, CommonConstants.ALERT_STATUS_PENDING); + labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(2L)); + + SingleAlert alert = new SingleAlert(); + alert.setContent("Alert cache manager test"); + alert.setLabels(labels); + + String fingerprint = AlertUtil.calculateFingerprint(alert.getLabels()); + alarmCacheManager.putPending(2L, fingerprint, alert); + + SingleAlert pendingSingleAlert = alarmCacheManager.getPending(2L, fingerprint); + assertNotNull(pendingSingleAlert); + alarmCacheManager.removePending(2L, fingerprint); + pendingSingleAlert = alarmCacheManager.getPending(2L, fingerprint); + assertNull(pendingSingleAlert); + } + + @Test + void testFiring() { + Map<String, String> labels = new HashMap<>(); + labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL); + labels.put(CommonConstants.ALERT_SEVERITY_INFO, CommonConstants.ALERT_STATUS_PENDING); + labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(3L)); + + SingleAlert alert = new SingleAlert(); + alert.setContent("Alert cache manager test"); + alert.setLabels(labels); + + String fingerprint = AlertUtil.calculateFingerprint(alert.getLabels()); + alarmCacheManager.putFiring(3L, fingerprint, alert); + + SingleAlert firingSingleAlert = alarmCacheManager.getFiring(3L, fingerprint); + assertNotNull(firingSingleAlert); + alarmCacheManager.removeFiring(3L, fingerprint); + firingSingleAlert = alarmCacheManager.getFiring(3L, fingerprint); + assertNull(firingSingleAlert); + } + + @Test + void testHistorical() { + SingleAlert alert = new SingleAlert(); + alert.setContent("Alert cache manager test"); + alert.setLabels(Collections.singletonMap(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL)); + when(singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING)).thenReturn(Collections.singletonList(alert)); + alarmCacheManager = new AlarmCacheManager(singleAlertDao); + + String fingerprint = AlertUtil.calculateFingerprint(alert.getLabels()); + SingleAlert historicalSingleAlert = alarmCacheManager.getFiring(4L, fingerprint); + assertNotNull(historicalSingleAlert); + SingleAlert singleAlert = alarmCacheManager.removeFiring(4L, fingerprint); + assertNotNull(singleAlert); + historicalSingleAlert = alarmCacheManager.getFiring(4L, fingerprint); + assertNull(historicalSingleAlert); + } +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/CollectorAlertHandlerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/CollectorAlertHandlerTest.java new file mode 100644 index 0000000..8de912a --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/CollectorAlertHandlerTest.java @@ -0,0 +1,147 @@ +/* + * 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.calculate; + +import org.apache.hertzbeat.alert.dao.AlertCollectorDao; +import org.apache.hertzbeat.alert.dao.SingleAlertDao; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.util.AlertUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.entity.manager.Collector; +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.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Collector alert handler test + */ +@ExtendWith(MockitoExtension.class) +public class CollectorAlertHandlerTest { + + @Mock + private AlertCollectorDao alertCollectorDao; + + @Mock + private SingleAlertDao singleAlertDao; + + @Mock + private AlarmCommonReduce alarmCommonReduce; + + private AlarmCacheManager alarmCacheManager; + + private CollectorAlertHandler collectorAlertHandler; + + @BeforeEach + void setUp() { + alarmCacheManager = Mockito.spy(new AlarmCacheManager(singleAlertDao)); + collectorAlertHandler = new CollectorAlertHandler(alarmCommonReduce, alertCollectorDao, alarmCacheManager); + } + + @Test + public void testOnline() { + String identity = "localhost-collector"; + Collector collector = Collector.builder() + .name(identity) + .ip("127.0.0.1") + .version("1.0") + .build(); + SingleAlert singleAlert = SingleAlert.builder().build(); + + when(alertCollectorDao.findCollectorByName(any(String.class))).thenReturn(collector); + when(alarmCacheManager.removeFiring(any(String.class))).thenReturn(singleAlert); + + collectorAlertHandler.online(identity); + + assertEquals(CommonConstants.ALERT_STATUS_RESOLVED, singleAlert.getStatus()); + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + public void testOffline() { + String identity = "localhost-collector"; + Collector collector = Collector.builder() + .name(identity) + .ip("127.0.0.1") + .version("1.0") + .build(); + + when(alertCollectorDao.findCollectorByName(any(String.class))).thenReturn(collector); + when(alarmCacheManager.getFiring(any(String.class))).thenReturn(null); + + collectorAlertHandler.offline(identity); + + verify(alarmCacheManager, times(1)).putFiring(any(String.class), any(SingleAlert.class)); + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testOfflineAndOnlineLifecycle() { + String identity = "localhost-collector"; + Collector collector = Collector.builder() + .name(identity) + .ip("127.0.0.1") + .version("1.0") + .build(); + + Map<String, String> fingerPrints = new HashMap<>(8); + fingerPrints.put("collectorName", collector.getName()); + fingerPrints.put("collectorVersion", collector.getVersion()); + fingerPrints.put("collectorHost", collector.getIp()); + String fingerprint = AlertUtil.calculateFingerprint(fingerPrints); + + when(alertCollectorDao.findCollectorByName(identity)).thenReturn(collector); + + // step1. first time offline + collectorAlertHandler.offline(identity); + assertNotNull(alarmCacheManager.getFiring(fingerprint)); + + // step2. online + collectorAlertHandler.online(identity); + assertNull(alarmCacheManager.getFiring(fingerprint)); + + // step3. second time offline + collectorAlertHandler.offline(identity); + assertNotNull(alarmCacheManager.getFiring(fingerprint)); + + // Verify that the push status is correct. + ArgumentCaptor<SingleAlert> captor = ArgumentCaptor.forClass(SingleAlert.class); + verify(alarmCommonReduce, atLeast(3)).reduceAndSendAlarm(captor.capture()); + List<SingleAlert> alerts = captor.getAllValues(); + assertEquals(CommonConstants.ALERT_STATUS_FIRING, alerts.get(0).getStatus()); + assertEquals(CommonConstants.ALERT_STATUS_RESOLVED, alerts.get(1).getStatus()); + assertEquals(CommonConstants.ALERT_STATUS_FIRING, alerts.get(2).getStatus()); + } +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculatorTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculatorTest.java new file mode 100644 index 0000000..39a9fcd --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/LogPeriodicAlertCalculatorTest.java @@ -0,0 +1,252 @@ +/* + * 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.calculate.periodic; + +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.DataSourceService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertAll; +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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Test case for {@link LogPeriodicAlertCalculator} + */ +@ExtendWith(MockitoExtension.class) +class LogPeriodicAlertCalculatorTest { + + @Mock + private DataSourceService dataSourceService; + + @Mock + private AlarmCommonReduce alarmCommonReduce; + + @InjectMocks + private LogPeriodicAlertCalculator calculator; + + private AlertDefine groupAlertDefine; + private AlertDefine individualAlertDefine; + + @BeforeEach + void setUp() { + groupAlertDefine = AlertDefine.builder() + .id(1L) + .name("log_error_alert") + .type("periodic_log") + .expr("SELECT * FROM hertzbeat_logs WHERE severity_text = 'ERROR' AND time_unix_nano >= ? AND time_unix_nano <= ?") + .labels(Map.of(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_WARNING, "team", "backend", CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP)) + .annotations(Map.of("summary", "Error logs detected", "description", "Multiple error logs found")) + .template("Found ${severity_text} log: ${body} from ${service_name}") + .datasource("sql") + .enable(true) + .period(300) + .build(); + + individualAlertDefine = AlertDefine.builder() + .id(1L) + .name("log_error_alert") + .type("periodic_log") + .expr("SELECT * FROM hertzbeat_logs WHERE severity_text = 'ERROR' AND time_unix_nano >= ? AND time_unix_nano <= ?") + .labels(Map.of(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_WARNING, "team", "backend", + CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL)) + .annotations(Map.of("summary", "Error logs detected", "description", "Multiple error logs found")) + .template("Found ${severity_text} log: ${body} from ${service_name}") + .datasource("sql") + .enable(true) + .period(300) + .build(); + } + + @Test + void testCalculateWithEnabledRuleAndValidExpression() { + // Given + List<Map<String, Object>> queryResults = createSampleLogResults(3); + when(dataSourceService.query(anyString(), anyString())).thenReturn(queryResults); + + // When + calculator.calculate(groupAlertDefine); + + // Then + verify(dataSourceService).query("sql", "SELECT * FROM hertzbeat_logs WHERE severity_text = 'ERROR' AND time_unix_nano >= ? AND time_unix_nano <= ?"); + verify(alarmCommonReduce).reduceAndSendAlarmGroup(any(), any()); + } + + @Test + void testCalculateWithEmptyQueryResults() { + // Given + when(dataSourceService.query(anyString(), anyString())).thenReturn(new ArrayList<>()); + + // When + calculator.calculate(groupAlertDefine); + + // Then + verify(dataSourceService).query(anyString(), anyString()); + verifyNoInteractions(alarmCommonReduce); + } + + @Test + void testCalculateWithNullQueryResults() { + // Given + when(dataSourceService.query(anyString(), anyString())).thenReturn(null); + + // When + calculator.calculate(groupAlertDefine); + + // Then + verify(dataSourceService).query(anyString(), anyString()); + verifyNoInteractions(alarmCommonReduce); + } + + @Test + void testCalculateWithQueryException() { + // Given + when(dataSourceService.query(anyString(), anyString())).thenThrow(new RuntimeException("Database connection failed")); + + // When + calculator.calculate(groupAlertDefine); + + // Then + verify(dataSourceService).query(anyString(), anyString()); + verifyNoInteractions(alarmCommonReduce); + } + + @Test + void testIndividualAlertGeneration() { + // Given + Map<String, Object> logContext = createSingleLogResult("Error in payment service", "ERROR"); + List<Map<String, Object>> queryResults = List.of(logContext); + when(dataSourceService.query(anyString(), anyString())).thenReturn(queryResults); + + try (MockedStatic<org.apache.hertzbeat.alert.util.AlertTemplateUtil> mockedTemplateUtil = + Mockito.mockStatic(org.apache.hertzbeat.alert.util.AlertTemplateUtil.class)) { + + mockedTemplateUtil.when(() -> org.apache.hertzbeat.alert.util.AlertTemplateUtil.render(anyString(), any())) + .thenReturn("Rendered alert content"); + + // When + calculator.calculate(individualAlertDefine); + + // Then + ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class); + verify(alarmCommonReduce).reduceAndSendAlarm(alertCaptor.capture()); + + SingleAlert capturedAlert = alertCaptor.getValue(); + assertAll( + () -> assertEquals(CommonConstants.ALERT_STATUS_FIRING, capturedAlert.getStatus()), + () -> assertEquals(1, capturedAlert.getTriggerTimes()), + () -> assertEquals("Rendered alert content", capturedAlert.getContent()), + () -> assertNotNull(capturedAlert.getStartAt()), + () -> assertNotNull(capturedAlert.getActiveAt()), + () -> assertTrue(capturedAlert.getLabels().containsKey("alertname")), + () -> assertTrue(capturedAlert.getLabels().containsKey("defineid")), + () -> assertEquals("log_error_alert", capturedAlert.getLabels().get("alertname")), + () -> assertEquals("1", capturedAlert.getLabels().get("defineid")) + ); + } + } + + @Test + void testGroupAlertGeneration() { + // Given + List<Map<String, Object>> queryResults = createSampleLogResults(2); + when(dataSourceService.query(anyString(), anyString())).thenReturn(queryResults); + + try (MockedStatic<org.apache.hertzbeat.alert.util.AlertTemplateUtil> mockedTemplateUtil = + Mockito.mockStatic(org.apache.hertzbeat.alert.util.AlertTemplateUtil.class)) { + + mockedTemplateUtil.when(() -> org.apache.hertzbeat.alert.util.AlertTemplateUtil.render(anyString(), any())) + .thenReturn("Rendered alert content"); + + // When + calculator.calculate(groupAlertDefine); + + // Then + @SuppressWarnings("unchecked") + ArgumentCaptor<Map<String, String>> groupLabelsCaptor = ArgumentCaptor.forClass(Map.class); + @SuppressWarnings("unchecked") + ArgumentCaptor<List<SingleAlert>> alertsCaptor = ArgumentCaptor.forClass(List.class); + verify(alarmCommonReduce).reduceAndSendAlarmGroup(groupLabelsCaptor.capture(), alertsCaptor.capture()); + + Map<String, String> groupLabels = groupLabelsCaptor.getValue(); + List<SingleAlert> alerts = alertsCaptor.getValue(); + + assertAll( + () -> assertEquals("2", groupLabels.get("__rows__")), + () -> assertEquals(CommonConstants.ALERT_MODE_GROUP, groupLabels.get(CommonConstants.ALERT_MODE_LABEL)), + () -> assertEquals("log_error_alert", groupLabels.get("alertname")), + () -> assertEquals("1", groupLabels.get("defineid")), + () -> assertEquals(2, alerts.size()), + () -> alerts.forEach(alert -> { + assertEquals(CommonConstants.ALERT_STATUS_FIRING, alert.getStatus()); + assertEquals(2, alert.getTriggerTimes()); + assertNotNull(alert.getStartAt()); + assertNotNull(alert.getActiveAt()); + }) + ); + } + } + + /** + * Create sample log results with simplified structure - only key fields + */ + private List<Map<String, Object>> createSampleLogResults(int count) { + List<Map<String, Object>> results = new ArrayList<>(); + for (int i = 0; i < count; i++) { + results.add(createSingleLogResult("Error message " + i, "ERROR")); + } + return results; + } + + /** + * Create a single log result with simplified structure + */ + private Map<String, Object> createSingleLogResult(String message, String severityText) { + Map<String, Object> logEntry = new HashMap<>(); + + // Only essential fields for testing + logEntry.put("time_unix_nano", System.nanoTime()); + logEntry.put("severity_text", severityText); + logEntry.put("body", message); + logEntry.put("service_name", "payment-service"); + + return logEntry; + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/MetricsPeriodicAlertCalculatorTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/MetricsPeriodicAlertCalculatorTest.java new file mode 100644 index 0000000..ea87930 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/MetricsPeriodicAlertCalculatorTest.java @@ -0,0 +1,156 @@ +/* + * 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.calculate.periodic; + +import org.apache.hertzbeat.alert.calculate.AlarmCacheManager; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.DataSourceService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertAll; +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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Test case for {@link MetricsPeriodicAlertCalculator} + */ +@ExtendWith(MockitoExtension.class) +class MetricsPeriodicAlertCalculatorTest { + + @Mock + private DataSourceService dataSourceService; + + @Mock + private AlarmCommonReduce alarmCommonReduce; + + @Mock + private AlarmCacheManager alarmCacheManager; + + @InjectMocks + private MetricsPeriodicAlertCalculator periodicAlertCalculator; + + private AlertDefine rule; + + @BeforeEach + void setUp() { + rule = AlertDefine.builder() + .id(1L) + .name("high_cpu_usage") + .type("periodic") + .expr("usage>90") + .times(1) + .labels(Map.of("team", "test-team", "priority", "P1")) + .annotations(Map.of("summary", "High CPU usage detected")) + .template("High CPU usage (${__value__}%)") + .datasource("PROMETHEUS") + .enable(true) + .period(300) + .build(); + } + + @Test + void testTriggerAlertWhenMatchThreshold() { + reset(alarmCacheManager); + Map<String, Object> result = new HashMap<>(); + result.put("__value__", 95.0); // Non-null, matched with threshold + result.put("__timestamp__", System.currentTimeMillis()); + when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result)); + when(alarmCacheManager.getPending(eq(rule.getId()), anyString())).thenReturn(null); + periodicAlertCalculator.calculate(rule); + // Verify that putFiring is called + ArgumentCaptor<String> idCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class); + verify(alarmCacheManager).putFiring(eq(rule.getId()), idCaptor.capture(), alertCaptor.capture()); + // Assertion alarm status and content + SingleAlert alert = alertCaptor.getValue(); + assertAll(() -> assertEquals(CommonConstants.ALERT_STATUS_FIRING, alert.getStatus()), + () -> assertEquals("High CPU usage (95.0%)", alert.getContent())); + } + + @Test + void testNoTriggerAlertWhenNotMatchThreshold() { + reset(alarmCacheManager); + Map<String, Object> result = new HashMap<>(); + result.put("__value__", null); // null, not matched with threshold + result.put("__timestamp__", System.currentTimeMillis()); + when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result)); + periodicAlertCalculator.calculate(rule); + verify(alarmCacheManager, times(0)).putFiring(any(), any(), any()); + } + + @Test + void testResolveFiringAlert() { + reset(alarmCacheManager); + Map<String, Object> result = new HashMap<>(); + result.put("__value__", null); // null, not matched with threshold + result.put("__timestamp__", System.currentTimeMillis()); + SingleAlert pendingAlert = SingleAlert.builder() + .status(CommonConstants.ALERT_STATUS_FIRING) + .triggerTimes(2).startAt(System.currentTimeMillis() - 60000) + .activeAt(System.currentTimeMillis() - 30000) + .build(); + when(alarmCacheManager.removeFiring(eq(rule.getId()), anyString())).thenReturn(pendingAlert); + when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result)); + periodicAlertCalculator.calculate(rule); + ArgumentCaptor<SingleAlert> resolvedCaptor = ArgumentCaptor.forClass(SingleAlert.class); + verify(alarmCommonReduce).reduceAndSendAlarm(resolvedCaptor.capture()); + SingleAlert resolvedAlert = resolvedCaptor.getValue(); + assertAll(() -> assertEquals(CommonConstants.ALERT_STATUS_RESOLVED, resolvedAlert.getStatus()), + () -> assertNotNull(resolvedAlert.getEndAt()), + () -> assertTrue(resolvedAlert.getEndAt() > resolvedAlert.getStartAt())); + } + + @ParameterizedTest + @NullAndEmptySource + void testSkipWhenDataSourceResultInvalid(List<Map<String, Object>> results) { + when(dataSourceService.calculate(anyString(), anyString())).thenReturn(results); + periodicAlertCalculator.calculate(rule); + verifyNoInteractions(alarmCacheManager); + } + + @Test + void testHandleDisabledRule() { + rule.setEnable(false); + periodicAlertCalculator.calculate(rule); + verifyNoInteractions(dataSourceService); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleSchedulerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleSchedulerTest.java new file mode 100644 index 0000000..a18b811 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/periodic/PeriodicAlertRuleSchedulerTest.java @@ -0,0 +1,213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.calculate.periodic; + +import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.hertzbeat.alert.dao.AlertDefineDao; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.junit.jupiter.api.AfterEach; +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; + +/** + * Tests for {@link PeriodicAlertRuleScheduler}. + */ +@ExtendWith(MockitoExtension.class) +class PeriodicAlertRuleSchedulerTest { + + @Mock + private MetricsPeriodicAlertCalculator metricsCalculator; + + @Mock + private LogPeriodicAlertCalculator logCalculator; + + @Mock + private AlertDefineDao alertDefineDao; + + private PeriodicAlertRuleScheduler scheduler; + + @BeforeEach + void setUp() { + scheduler = new PeriodicAlertRuleScheduler(metricsCalculator, logCalculator, alertDefineDao, + VirtualThreadProperties.defaults()); + } + + @AfterEach + void tearDown() { + if (scheduler != null) { + scheduler.destroy(); + } + } + + @Test + void updateScheduleRunsPeriodicCalculationOnVirtualThread() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + doAnswer(invocation -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + return null; + }).when(metricsCalculator).calculate(any(AlertDefine.class)); + + AlertDefine rule = metricRule(1L); + scheduler.updateSchedule(rule); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void updateScheduleKeepsSingleInFlightExecutionPerRule() throws InterruptedException { + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + AtomicInteger concurrent = new AtomicInteger(); + AtomicInteger maxConcurrent = new AtomicInteger(); + AtomicInteger invocations = new AtomicInteger(); + doAnswer(invocation -> { + int active = concurrent.incrementAndGet(); + maxConcurrent.updateAndGet(current -> Math.max(current, active)); + int count = invocations.incrementAndGet(); + try { + if (count == 1) { + firstStarted.countDown(); + releaseFirst.await(5, TimeUnit.SECONDS); + } else if (count == 2) { + secondStarted.countDown(); + } + } finally { + concurrent.decrementAndGet(); + } + return null; + }).when(metricsCalculator).calculate(any(AlertDefine.class)); + + scheduler.updateSchedule(metricRule(2L)); + + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + Thread.sleep(1200L); + assertEquals(1, maxConcurrent.get()); + + releaseFirst.countDown(); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + assertEquals(1, maxConcurrent.get()); + } + + @Test + void cancelScheduleInterruptsRunningVirtualTask() throws InterruptedException { + CountDownLatch started = new CountDownLatch(1); + CountDownLatch interrupted = new CountDownLatch(1); + doAnswer(invocation -> { + started.countDown(); + try { + Thread.sleep(5000L); + } catch (InterruptedException e) { + interrupted.countDown(); + Thread.currentThread().interrupt(); + } + return null; + }).when(metricsCalculator).calculate(any(AlertDefine.class)); + + AlertDefine rule = metricRule(3L); + scheduler.updateSchedule(rule); + + assertTrue(started.await(5, TimeUnit.SECONDS)); + scheduler.cancelSchedule(rule.getId()); + assertTrue(interrupted.await(5, TimeUnit.SECONDS)); + } + + @Test + void updateScheduleHonorsConfiguredGlobalPeriodicConcurrencyLimit() throws InterruptedException { + scheduler.destroy(); + scheduler = new PeriodicAlertRuleScheduler(metricsCalculator, logCalculator, alertDefineDao, + periodicProperties(1)); + + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + AtomicInteger concurrent = new AtomicInteger(); + AtomicInteger maxConcurrent = new AtomicInteger(); + doAnswer(invocation -> { + int active = concurrent.incrementAndGet(); + maxConcurrent.updateAndGet(current -> Math.max(current, active)); + AlertDefine rule = invocation.getArgument(0); + try { + if (rule.getId().equals(4L)) { + firstStarted.countDown(); + releaseFirst.await(5, TimeUnit.SECONDS); + } else if (rule.getId().equals(5L)) { + secondStarted.countDown(); + } + } finally { + concurrent.decrementAndGet(); + } + return null; + }).when(metricsCalculator).calculate(any(AlertDefine.class)); + + scheduler.updateSchedule(metricRule(4L)); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + scheduler.updateSchedule(metricRule(5L)); + Thread.sleep(200L); + assertEquals(1, maxConcurrent.get()); + assertEquals(1L, secondStarted.getCount()); + + releaseFirst.countDown(); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + assertEquals(1, maxConcurrent.get()); + } + + private AlertDefine metricRule(Long id) { + return AlertDefine.builder() + .id(id) + .name("periodic-rule-" + id) + .type(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC) + .period(1) + .enable(true) + .build(); + } + + private VirtualThreadProperties periodicProperties(int maxConcurrentJobs) { + return new VirtualThreadProperties( + true, + VirtualThreadProperties.PoolProperties.collectorDefaults(), + VirtualThreadProperties.PoolProperties.commonDefaults(), + VirtualThreadProperties.PoolProperties.managerDefaults(), + new VirtualThreadProperties.AlerterProperties( + VirtualThreadProperties.PoolProperties.alerterNotifyDefaults(), + maxConcurrentJobs, + VirtualThreadProperties.QueueProperties.logWorkerDefaults(), + VirtualThreadProperties.QueueProperties.reduceDefaults(), + VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(), + 4), + VirtualThreadProperties.PoolProperties.warehouseDefaults(), + VirtualThreadProperties.AsyncProperties.defaults()); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/MetricsRealTimeAlertCalculatorMatchTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/MetricsRealTimeAlertCalculatorMatchTest.java new file mode 100644 index 0000000..6844bb1 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/MetricsRealTimeAlertCalculatorMatchTest.java @@ -0,0 +1,257 @@ +/* + * 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.calculate.realtime; + +import com.google.common.collect.Lists; +import org.apache.hertzbeat.alert.AlerterWorkerPool; +import org.apache.hertzbeat.alert.calculate.AlarmCacheManager; +import org.apache.hertzbeat.alert.calculate.JexlExprCalculator; +import org.apache.hertzbeat.alert.dao.SingleAlertDao; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.constants.MetricDataConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.common.queue.impl.InMemoryCommonDataQueue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * + */ +public class MetricsRealTimeAlertCalculatorMatchTest { + + private final AlerterWorkerPool workerPool = new AlerterWorkerPool(); + + @Mock + private CommonDataQueue dataQueue = new InMemoryCommonDataQueue(); + + @Mock + private AlertDefineService alertDefineService; + + @Mock + private SingleAlertDao singleAlertDao; + + @Mock + private AlarmCommonReduce alarmCommonReduce; + + @Mock + private AlarmCacheManager alarmCacheManager; + + private MetricsRealTimeAlertCalculator metricsRealTimeAlertCalculator; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + when(singleAlertDao.querySingleAlertsByStatus(any())).thenReturn(new ArrayList<>()); + metricsRealTimeAlertCalculator = new MetricsRealTimeAlertCalculator( + workerPool, + dataQueue, + alertDefineService, + singleAlertDao, + alarmCommonReduce, + alarmCacheManager, + new JexlExprCalculator(), + false + ); + } + + @Test + void testFilterThresholdsByAppAndMetrics_withInstanceExpr_HasSpace() { + + String app = "redis"; + String instanceId = "501045327364864"; + int priority = 0; + + AlertDefine matchDefine = new AlertDefine(); + matchDefine.setExpr("equals(__app__,\"redis\") && equals(__instance__, \"501045327364864\")"); + + AlertDefine unmatchDefine = new AlertDefine(); + unmatchDefine.setExpr("equals(__app__,\"redis\") && equals(__instance__, \"999999999\")"); + + List<AlertDefine> allDefines = Collections.singletonList(matchDefine); + + List<AlertDefine> filtered = metricsRealTimeAlertCalculator.filterThresholdsByAppAndMetrics(allDefines, app, "", Map.of(), instanceId, priority); + + // It should filter out 999999999. + assertEquals(1, filtered.size()); + assertEquals("equals(__app__,\"redis\") && equals(__instance__, \"501045327364864\")", + filtered.get(0).getExpr()); + } + + @Test + void testPrometheusReplaceMultipleJobsApp() throws InterruptedException { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + builder.setId(518789738974464L) + .setApp("_prometheus_Cool_Stingray_34Nj_copy") + .setMetrics("canal_instance") + .setPriority(0) + .setCode(CollectRep.Code.SUCCESS); + + CollectRep.Field destination = CollectRep.Field.newBuilder().setName("destination").setType(CommonConstants.TYPE_STRING).setLabel(true).build(); + CollectRep.Field mode = CollectRep.Field.newBuilder().setName("mode").setType(CommonConstants.TYPE_STRING).setLabel(true).build(); + CollectRep.Field metricValue = CollectRep.Field.newBuilder().setName("metric_value").setType(CommonConstants.TYPE_NUMBER).setLabel(true).build(); + + Map<String, String> meta = new HashMap<>(); + meta.put(MetricDataConstants.INSTANCE_NAME, "Cool_Stingray_34Nj_copy"); + meta.put(MetricDataConstants.INSTANCE, "127.0.0.1"); + + builder.addMetadataAll(meta); + builder.addAllFields(Lists.newArrayList(destination, mode, metricValue)); + builder.addValueRow(CollectRep.ValueRow.newBuilder().addColumn("example").addColumn("spring").addColumn("1.0").build()); + + CollectRep.MetricsData metricsData = builder.build(); + + + AlertDefine matchDefine = new AlertDefine(); + matchDefine.setId(1L); + matchDefine.setName("test"); + matchDefine.setExpr( + "equals(__app__,\"prometheus\") && " + + "equals(__metrics__,\"canal_instance\") && " + + "(equals(__instance__, \"515224274242816\") or equals(__instance__, \"518789738974464\")) && " + + "metric_value > 0" + ); + matchDefine.setTemplate("Canal instance val: ${value}%"); + matchDefine.setTimes(1); + + List<AlertDefine> allDefines = Collections.singletonList(matchDefine); + + when(alertDefineService.getMetricsRealTimeAlertDefines()).thenReturn(allDefines); + when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException()); + + metricsRealTimeAlertCalculator.startCalculate(); + + Thread.sleep(3000); + + verify(alarmCacheManager, times(1)).getPending(any(), any()); + verify(alarmCacheManager, times(1)).putFiring(any(), any(), any()); + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any()); + } + + @Test + void testPrometheusReplaceApp() throws InterruptedException { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + builder.setId(1) + .setApp("_prometheus_Cool_Stingray_34Nj") + .setMetrics("canal_instance") + .setPriority(0) + .setCode(CollectRep.Code.SUCCESS); + + CollectRep.Field destination = CollectRep.Field.newBuilder().setName("destination").setType(CommonConstants.TYPE_STRING).setLabel(true).build(); + CollectRep.Field mode = CollectRep.Field.newBuilder().setName("mode").setType(CommonConstants.TYPE_STRING).setLabel(true).build(); + CollectRep.Field metricValue = CollectRep.Field.newBuilder().setName("metric_value").setType(CommonConstants.TYPE_NUMBER).setLabel(true).build(); + + Map<String, String> meta = new HashMap<>(); + meta.put(MetricDataConstants.INSTANCE_NAME, "Cool_Stingray_34Nj"); + meta.put(MetricDataConstants.INSTANCE, "127.0.0.1"); + + builder.addMetadataAll(meta); + builder.addAllFields(Lists.newArrayList(destination, mode, metricValue)); + builder.addValueRow(CollectRep.ValueRow.newBuilder().addColumn("example").addColumn("spring").addColumn("1.0").build()); + + CollectRep.MetricsData metricsData = builder.build(); + + AlertDefine matchDefine = new AlertDefine(); + matchDefine.setId(1L); + matchDefine.setName("test"); + matchDefine.setExpr("equals(__app__,\"prometheus\") && equals(__metrics__,\"canal_instance\") && metric_value > 0"); + matchDefine.setTemplate("Canal instance val: ${value}%"); + matchDefine.setTimes(1); + + List<AlertDefine> allDefines = Collections.singletonList(matchDefine); + + when(alertDefineService.getMetricsRealTimeAlertDefines()).thenReturn(allDefines); + when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException()); + + metricsRealTimeAlertCalculator.startCalculate(); + + Thread.sleep(3000); + + verify(alarmCacheManager, times(1)).getPending(any(), any()); + verify(alarmCacheManager, times(1)).putFiring(any(), any(), any()); + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any()); + } + + @Test + void testCalculateWithNormalApp() throws InterruptedException { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + builder.setId(1) + .setApp("springboot3") + .setMetrics("available") + .setPriority(0) + .setCode(CollectRep.Code.SUCCESS) + .setTenantId(0).setId(518679137103104L) + .setTime(1749110170834L) + .setPriority(0); + + CollectRep.Field responseTime = CollectRep.Field.newBuilder() + .setName("responseTime") + .setType(CommonConstants.TYPE_STRING) + .setUnit("ms") + .setLabel(false) + .build(); + + Map<String, String> meta = new HashMap<>(); + meta.put(MetricDataConstants.INSTANCE_NAME, "Vibrant_Gazelle_83vJ"); + meta.put(MetricDataConstants.INSTANCE, "127.0.0.1"); + + builder.addMetadataAll(meta); + builder.addAllFields(Lists.newArrayList(responseTime)); + builder.addValueRow(CollectRep.ValueRow.newBuilder().addColumn("18").build()); + + CollectRep.MetricsData metricsData = builder.build(); + + AlertDefine matchDefine = new AlertDefine(); + matchDefine.setId(1L); + matchDefine.setName("test"); + matchDefine.setExpr("equals(__app__,\"springboot3\") && equals(__metrics__,\"available\") && equals(__instance__, \"518679137103104\") && responseTime > 0"); + matchDefine.setTemplate("Canal instance val: ${value}%"); + matchDefine.setTimes(1); + + List<AlertDefine> allDefines = Collections.singletonList(matchDefine); + + when(alertDefineService.getMetricsRealTimeAlertDefines()).thenReturn(allDefines); + when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException()); + + metricsRealTimeAlertCalculator.startCalculate(); + + Thread.sleep(3000); + + verify(alarmCacheManager, times(1)).getPending(any(), any()); + verify(alarmCacheManager, times(1)).putFiring(any(), any(), any()); + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any()); + } + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/MetricsRealTimeAlertCalculatorTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/MetricsRealTimeAlertCalculatorTest.java new file mode 100644 index 0000000..3d4b411 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/MetricsRealTimeAlertCalculatorTest.java @@ -0,0 +1,103 @@ +/* + * 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.calculate.realtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.hertzbeat.alert.AlerterWorkerPool; +import org.apache.hertzbeat.alert.calculate.AlarmCacheManager; +import org.apache.hertzbeat.alert.calculate.JexlExprCalculator; +import org.apache.hertzbeat.alert.dao.SingleAlertDao; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + + +class MetricsRealTimeAlertCalculatorTest { + + private MetricsRealTimeAlertCalculator calculator; + + @BeforeEach + void setUp() { + AlerterWorkerPool mockPool = Mockito.mock(AlerterWorkerPool.class); + CommonDataQueue mockQueue = Mockito.mock(CommonDataQueue.class); + AlertDefineService mockAlertDefineService = Mockito.mock(AlertDefineService.class); + SingleAlertDao mockDao = Mockito.mock(SingleAlertDao.class); + AlarmCommonReduce mockReduce = Mockito.mock(AlarmCommonReduce.class); + AlarmCacheManager alarmCacheManager = Mockito.mock(AlarmCacheManager.class); + + Mockito.when(mockDao.querySingleAlertsByStatus(Mockito.anyString())) + .thenReturn(Collections.emptyList()); + + calculator = new MetricsRealTimeAlertCalculator(mockPool, mockQueue, mockAlertDefineService + , mockDao, mockReduce, alarmCacheManager, new JexlExprCalculator(), false); + } + + @Test + void testFilterThresholdsByAppAndMetrics_withInstanceExpr_HasSpace() { + String app = "redis"; + String instanceId = "501045327364864"; + int priority = 0; + + AlertDefine matchDefine = new AlertDefine(); + matchDefine.setExpr("equals(__app__,\"redis\") && equals(__instance__, \"501045327364864\")"); + + AlertDefine unmatchDefine = new AlertDefine(); + unmatchDefine.setExpr("equals(__app__,\"redis\") && equals(__instance__, \"999999999\")"); + + List<AlertDefine> allDefines = Arrays.asList(matchDefine, unmatchDefine); + + List<AlertDefine> filtered = calculator.filterThresholdsByAppAndMetrics(allDefines, app, "", Map.of(), instanceId, priority); + + // It should filter out 999999999. + assertEquals(1, filtered.size()); + assertEquals("equals(__app__,\"redis\") && equals(__instance__, \"501045327364864\")", + filtered.get(0).getExpr()); + } + + @Test + void testFilterThresholdsByAppAndMetrics_withInstanceExpr_NoSpace() { + String app = "redis"; + String instanceId = "501045327364864"; + int priority = 0; + + AlertDefine matchDefine = new AlertDefine(); + matchDefine.setExpr("equals(__app__,\"redis\") && equals(__instance__,\"501045327364864\")"); + + AlertDefine unmatchDefine = new AlertDefine(); + unmatchDefine.setExpr("equals(__app__,\"redis\") && equals(__instance__,\"999999999\")"); + + List<AlertDefine> allDefines = Arrays.asList(matchDefine, unmatchDefine); + + List<AlertDefine> filtered = calculator.filterThresholdsByAppAndMetrics(allDefines, app, "", Map.of(), instanceId, priority); + + // It should filter out 999999999. + assertEquals(1, filtered.size()); + assertEquals("equals(__app__,\"redis\") && equals(__instance__,\"501045327364864\")", + filtered.get(0).getExpr()); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/WindowedLogRealTimeAlertCalculatorTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/WindowedLogRealTimeAlertCalculatorTest.java new file mode 100644 index 0000000..e07f573 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/WindowedLogRealTimeAlertCalculatorTest.java @@ -0,0 +1,186 @@ +/* + * 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.calculate.realtime; + +import org.apache.hertzbeat.alert.calculate.realtime.window.LogWorker; +import org.apache.hertzbeat.alert.calculate.realtime.window.TimeService; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test case for {@link WindowedLogRealTimeAlertCalculator} + */ +@ExtendWith(MockitoExtension.class) +class WindowedLogRealTimeAlertCalculatorTest { + + @Mock + private CommonDataQueue dataQueue; + + @Mock + private TimeService timeService; + + @Mock + private LogWorker logWorker; + + @InjectMocks + private WindowedLogRealTimeAlertCalculator calculator; + + private LogEntry validLogEntry; + private LogEntry invalidTimestampLogEntry; + private LogEntry lateDataLogEntry; + + @BeforeEach + void setUp() { + // Create valid log entry with current timestamp + long currentTimeNano = System.currentTimeMillis() * 1_000_000L; + validLogEntry = LogEntry.builder() + .timeUnixNano(currentTimeNano) + .severityText("ERROR") + .body("Test error message") + .build(); + + // Create log entry with invalid timestamp (far future) + long invalidTimeNano = (System.currentTimeMillis() + 2 * 60 * 60 * 1000L) * 1_000_000L; // 2 hours in future + invalidTimestampLogEntry = LogEntry.builder() + .timeUnixNano(invalidTimeNano) + .severityText("ERROR") + .body("Test error message with invalid timestamp") + .build(); + + // Create late data log entry + long lateTimeNano = (System.currentTimeMillis() - 60 * 60 * 1000L) * 1_000_000L; // 1 hour in past + lateDataLogEntry = LogEntry.builder() + .timeUnixNano(lateTimeNano) + .severityText("ERROR") + .body("Test late data message") + .build(); + } + + @Test + void testRunWithValidLogEntry() throws InterruptedException { + // Given + when(dataQueue.pollLogEntry()).thenReturn(validLogEntry, (LogEntry) null); + when(timeService.isValidTimestamp(anyLong())).thenReturn(true); + when(timeService.isLateData(anyLong())).thenReturn(false); + + // Create a thread to run the calculator + Thread calculatorThread = new Thread(calculator); + + // When + calculatorThread.start(); + + // Give some time for processing + Thread.sleep(100); + calculatorThread.interrupt(); + calculatorThread.join(1000); + + // Then + verify(dataQueue, atLeastOnce()).pollLogEntry(); + verify(timeService).isValidTimestamp(anyLong()); + verify(timeService).isLateData(anyLong()); + verify(timeService).updateMaxTimestamp(anyLong()); + verify(logWorker).reduceAndSendLogTask(validLogEntry); + } + + @Test + void testRunWithInvalidTimestamp() throws InterruptedException { + // Given + when(dataQueue.pollLogEntry()).thenReturn(invalidTimestampLogEntry, (LogEntry) null); + when(timeService.isValidTimestamp(anyLong())).thenReturn(false); + + // Create a thread to run the calculator + Thread calculatorThread = new Thread(calculator); + + // When + calculatorThread.start(); + + // Give some time for processing + Thread.sleep(100); + calculatorThread.interrupt(); + calculatorThread.join(1000); + + // Then + verify(dataQueue, atLeastOnce()).pollLogEntry(); + verify(timeService).isValidTimestamp(anyLong()); + verify(timeService, never()).isLateData(anyLong()); + verify(timeService, never()).updateMaxTimestamp(anyLong()); + verify(logWorker, never()).reduceAndSendLogTask(any()); + } + + @Test + void testRunWithLateData() throws InterruptedException { + // Given + when(dataQueue.pollLogEntry()).thenReturn(lateDataLogEntry, (LogEntry) null); + when(timeService.isValidTimestamp(anyLong())).thenReturn(true); + when(timeService.isLateData(anyLong())).thenReturn(true); + when(timeService.getCurrentWatermark()).thenReturn(System.currentTimeMillis() - 30 * 60 * 1000L); // 30 minutes ago + + // Create a thread to run the calculator + Thread calculatorThread = new Thread(calculator); + + // When + calculatorThread.start(); + + // Give some time for processing + Thread.sleep(100); + calculatorThread.interrupt(); + calculatorThread.join(1000); + + // Then + verify(dataQueue, atLeastOnce()).pollLogEntry(); + verify(timeService).isValidTimestamp(anyLong()); + verify(timeService).isLateData(anyLong()); + verify(timeService, never()).updateMaxTimestamp(anyLong()); + verify(logWorker, never()).reduceAndSendLogTask(any()); + } + + @Test + void testRunWithGeneralException() throws InterruptedException { + // Given + when(dataQueue.pollLogEntry()).thenThrow(new RuntimeException("Test runtime exception")) + .thenReturn(null); // Return null to exit the loop + + // Create a thread to run the calculator + Thread calculatorThread = new Thread(calculator); + + // When + calculatorThread.start(); + + // Give some time for processing + Thread.sleep(100); + calculatorThread.interrupt(); + calculatorThread.join(1000); + + // Then + verify(dataQueue, atLeastOnce()).pollLogEntry(); + } + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluatorTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluatorTest.java new file mode 100644 index 0000000..d520821 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/AlarmEvaluatorTest.java @@ -0,0 +1,461 @@ +/* + * 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.calculate.realtime.window; + +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.junit.jupiter.api.AfterEach; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertAll; +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.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test case for {@link AlarmEvaluator} + */ +@ExtendWith(MockitoExtension.class) +class AlarmEvaluatorTest { + + @Mock + private AlarmCommonReduce alarmCommonReduce; + + @InjectMocks + private AlarmEvaluator alarmEvaluator; + + private AlertDefine alertDefine; + private WindowAggregator.WindowData windowData; + + @BeforeEach + void setUp() { + // Create test log entry + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .severityText("ERROR") + .body("Test error message") + .attributes(Map.of("service", "test-service", "pod", "test-pod")) + .resource(Map.of("cluster", "test-cluster")) + .build(); + + // Create test alert define for individual mode + alertDefine = AlertDefine.builder() + .id(1L) + .name("test_log_alert") + .type("realtime_log") + .expr("log.severityText == 'ERROR'") + .times(1) + .template("Alert: {{log.body}}") + .labels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL, CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL)) + .annotations(Map.of("summary", "Test alert summary")) + .enable(true) + .build(); + + // Create matching log events + MatchingLogEvent matchingEvent = MatchingLogEvent.builder() + .logEntry(logEntry) + .alertDefine(alertDefine) + .eventTimestamp(System.currentTimeMillis()) + .workerTimestamp(System.currentTimeMillis()) + .build(); + + // Create window data + WindowAggregator.WindowKey windowKey = new WindowAggregator.WindowKey(1L, + System.currentTimeMillis() - 60000, System.currentTimeMillis()); + windowData = new WindowAggregator.WindowData(windowKey, alertDefine); + windowData.addMatchingLog(matchingEvent); + } + + @AfterEach + void tearDown() { + if (alarmEvaluator != null) { + alarmEvaluator.destroy(); + } + } + + @Test + void testProcessWindowDataWithIndividualMode() throws InterruptedException { + // Given - alert define with individual mode + alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL)); + + // When + alarmEvaluator.sendAndProcessWindowData(windowData); + + // Wait for async processing + Thread.sleep(2000); + + // Then + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + verify(alarmCommonReduce, never()).reduceAndSendAlarmGroup(anyMap(), anyList()); + } + + @Test + void testProcessWindowDataWithGroupMode() throws InterruptedException { + // Given - alert define with group mode + alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP)); + + // When + alarmEvaluator.sendAndProcessWindowData(windowData); + + // Wait for async processing + Thread.sleep(2000); + + // Then + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + verify(alarmCommonReduce, times(1)).reduceAndSendAlarmGroup(anyMap(), anyList()); + } + + @Test + void testProcessWindowDataWithEmptyLogs() throws InterruptedException { + // Given - window data with no matching logs + WindowAggregator.WindowKey emptyWindowKey = new WindowAggregator.WindowKey(1L, + System.currentTimeMillis() - 60000, System.currentTimeMillis()); + WindowAggregator.WindowData emptyWindowData = new WindowAggregator.WindowData(emptyWindowKey, alertDefine); + + // When + alarmEvaluator.sendAndProcessWindowData(emptyWindowData); + + // Wait for async processing + Thread.sleep(2000); + + // Then - should not process any alerts + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + verify(alarmCommonReduce, never()).reduceAndSendAlarmGroup(anyMap(), anyList()); + } + + @Test + void testProcessWindowDataWithInsufficientTimes() throws InterruptedException { + // Given - alert define requiring 3 times but only 1 matching log + alertDefine.setTimes(3); + + // When + alarmEvaluator.sendAndProcessWindowData(windowData); + + // Wait for async processing + Thread.sleep(2000); + + // Then - should not process any alerts due to insufficient count + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + verify(alarmCommonReduce, never()).reduceAndSendAlarmGroup(anyMap(), anyList()); + } + + @Test + void testIndividualAlertGeneration() throws InterruptedException { + // Given + alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL, CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL)); + alertDefine.setAnnotations(Map.of("summary", "Test summary", "description", "Test description")); + + // When + alarmEvaluator.sendAndProcessWindowData(windowData); + + // Wait for async processing + Thread.sleep(2000); + + // Then + ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class); + verify(alarmCommonReduce).reduceAndSendAlarm(alertCaptor.capture()); + + SingleAlert capturedAlert = alertCaptor.getValue(); + assertAll( + () -> assertNotNull(capturedAlert), + () -> assertEquals(CommonConstants.ALERT_STATUS_FIRING, capturedAlert.getStatus()), + () -> assertEquals(1, capturedAlert.getTriggerTimes()), + () -> assertNotNull(capturedAlert.getContent()), + () -> assertNotNull(capturedAlert.getLabels()), + () -> assertNotNull(capturedAlert.getAnnotations()), + () -> assertTrue(capturedAlert.getStartAt() > 0), + () -> assertTrue(capturedAlert.getActiveAt() > 0) + ); + + // Verify labels contain alert define info and log entry data + Map<String, String> labels = capturedAlert.getLabels(); + assertTrue(labels.containsKey(CommonConstants.LABEL_ALERT_SEVERITY)); + assertTrue(labels.containsKey("severityText")); + assertTrue(labels.containsKey("body")); + } + + @Test + void testGroupAlertGeneration() throws InterruptedException { + // Given + alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP, CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_WARNING)); + + // Add more matching logs to test group functionality + MatchingLogEvent secondEvent = MatchingLogEvent.builder() + .logEntry(LogEntry.builder() + .timeUnixNano((System.currentTimeMillis() + 1000) * 1_000_000L) + .severityText("ERROR") + .body("Second error message") + .build()) + .alertDefine(alertDefine) + .eventTimestamp(System.currentTimeMillis() + 1000) + .workerTimestamp(System.currentTimeMillis() + 1000) + .build(); + + windowData.addMatchingLog(secondEvent); + + // When + alarmEvaluator.sendAndProcessWindowData(windowData); + + // Wait for async processing + Thread.sleep(2000); + + // Then + @SuppressWarnings("unchecked") + ArgumentCaptor<Map<String, String>> labelsCaptor = ArgumentCaptor.forClass(Map.class); + @SuppressWarnings("unchecked") + ArgumentCaptor<List<SingleAlert>> alertsCaptor = ArgumentCaptor.forClass(List.class); + verify(alarmCommonReduce).reduceAndSendAlarmGroup(labelsCaptor.capture(), alertsCaptor.capture()); + + Map<String, String> groupLabels = labelsCaptor.getValue(); + List<SingleAlert> alerts = alertsCaptor.getValue(); + + assertAll( + () -> assertNotNull(groupLabels), + () -> assertNotNull(alerts), + () -> assertEquals(2, alerts.size()), + () -> assertTrue(groupLabels.containsKey("window_start_time")), + () -> assertTrue(groupLabels.containsKey("window_end_time")), + () -> assertTrue(groupLabels.containsKey("matching_logs_count")), + () -> assertEquals(CommonConstants.ALERT_MODE_GROUP, groupLabels.get(CommonConstants.ALERT_MODE_LABEL)), + () -> assertEquals("2", groupLabels.get("matching_logs_count")) + ); + + // Verify each alert in the group + for (SingleAlert alert : alerts) { + assertEquals(CommonConstants.ALERT_STATUS_FIRING, alert.getStatus()); + assertEquals(2, alert.getTriggerTimes()); // Should equal the count of matching logs + } + } + + @Test + void testLogEntryDataMapping() throws InterruptedException { + // Given - log entry with comprehensive data + LogEntry detailedLogEntry = LogEntry.builder() + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .severityNumber(17) // ERROR level + .severityText("ERROR") + .body("Detailed error message") + .attributes(Map.of("service", "api-service", "version", "1.0.0")) + .resource(Map.of("cluster", "prod-cluster", "namespace", "default")) + .droppedAttributesCount(0) + .traceId("trace123") + .spanId("span456") + .traceFlags(1) + .build(); + + MatchingLogEvent detailedEvent = MatchingLogEvent.builder() + .logEntry(detailedLogEntry) + .alertDefine(alertDefine) + .eventTimestamp(System.currentTimeMillis()) + .workerTimestamp(System.currentTimeMillis()) + .build(); + + WindowAggregator.WindowData detailedWindowData = new WindowAggregator.WindowData( + new WindowAggregator.WindowKey(1L, System.currentTimeMillis() - 60000, System.currentTimeMillis()), + alertDefine); + detailedWindowData.addMatchingLog(detailedEvent); + + alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL)); + + // When + alarmEvaluator.sendAndProcessWindowData(detailedWindowData); + Thread.sleep(2000); + + // Then + ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class); + verify(alarmCommonReduce).reduceAndSendAlarm(alertCaptor.capture()); + + SingleAlert alert = alertCaptor.getValue(); + Map<String, String> labels = alert.getLabels(); + + // Verify log entry fields are mapped to labels + assertAll( + () -> assertEquals("17", labels.get("severityNumber")), + () -> assertEquals("ERROR", labels.get("severityText")), + () -> assertEquals("Detailed error message", labels.get("body")), + () -> assertEquals("0", labels.get("droppedAttributesCount")), + () -> assertEquals("trace123", labels.get("traceId")), + () -> assertEquals("span456", labels.get("spanId")), + () -> assertEquals("1", labels.get("traceFlags")), + () -> assertEquals("api-service", labels.get("attr_service")), + () -> assertEquals("1.0.0", labels.get("attr_version")), + () -> assertEquals("prod-cluster", labels.get("resource_cluster")), + () -> assertEquals("default", labels.get("resource_namespace")) + ); + } + + @Test + void testMultipleMatchingLogsInWindow() throws InterruptedException { + // Given - multiple matching logs in the same window + alertDefine.setTimes(2); // Require at least 2 occurrences + alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP)); + + // Add second matching log + MatchingLogEvent secondEvent = MatchingLogEvent.builder() + .logEntry(LogEntry.builder() + .severityText("ERROR") + .body("Another error") + .build()) + .alertDefine(alertDefine) + .eventTimestamp(System.currentTimeMillis() + 5000) + .workerTimestamp(System.currentTimeMillis() + 5000) + .build(); + + windowData.addMatchingLog(secondEvent); + + // When + alarmEvaluator.sendAndProcessWindowData(windowData); + Thread.sleep(2000); + + // Then - should trigger group alert since threshold is met + verify(alarmCommonReduce).reduceAndSendAlarmGroup(anyMap(), anyList()); + + @SuppressWarnings("unchecked") + ArgumentCaptor<List<SingleAlert>> alertsCaptor = ArgumentCaptor.forClass(List.class); + verify(alarmCommonReduce).reduceAndSendAlarmGroup(anyMap(), alertsCaptor.capture()); + + List<SingleAlert> alerts = alertsCaptor.getValue(); + assertEquals(2, alerts.size()); + assertEquals(2, alerts.get(0).getTriggerTimes()); // Each alert should have trigger times = total count + } + + @Test + void testSendAndProcessWindowDataRunsOnVirtualThread() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + doAnswer(invocation -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + return null; + }).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class)); + + alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL)); + alarmEvaluator.sendAndProcessWindowData(windowData); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void testSendAndProcessWindowDataQueuesWhenConcurrencyLimitReached() throws Exception { + VirtualThreadProperties properties = new VirtualThreadProperties( + true, + VirtualThreadProperties.PoolProperties.collectorDefaults(), + VirtualThreadProperties.PoolProperties.commonDefaults(), + VirtualThreadProperties.PoolProperties.managerDefaults(), + new VirtualThreadProperties.AlerterProperties( + VirtualThreadProperties.PoolProperties.alerterNotifyDefaults(), + 10, + VirtualThreadProperties.QueueProperties.logWorkerDefaults(), + VirtualThreadProperties.QueueProperties.reduceDefaults(), + new VirtualThreadProperties.QueueProperties(1, 0), + 4), + VirtualThreadProperties.PoolProperties.warehouseDefaults(), + VirtualThreadProperties.AsyncProperties.defaults()); + alarmEvaluator.destroy(); + alarmEvaluator = new AlarmEvaluator(alarmCommonReduce, properties); + + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + AtomicInteger invocationOrder = new AtomicInteger(); + doAnswer(invocation -> { + int order = invocationOrder.incrementAndGet(); + if (order == 1) { + firstStarted.countDown(); + try { + releaseFirst.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } else if (order == 2) { + secondStarted.countDown(); + } + return null; + }).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class)); + + WindowAggregator.WindowData firstWindow = cloneWindowDataWithBody("first"); + WindowAggregator.WindowData secondWindow = cloneWindowDataWithBody("second"); + firstWindow.getAlertDefine().setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL)); + secondWindow.getAlertDefine().setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL)); + + alarmEvaluator.sendAndProcessWindowData(firstWindow); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + alarmEvaluator.sendAndProcessWindowData(secondWindow); + assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS)); + + releaseFirst.countDown(); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + } + + private WindowAggregator.WindowData cloneWindowDataWithBody(String body) { + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .severityText("ERROR") + .body(body) + .build(); + AlertDefine define = AlertDefine.builder() + .id(alertDefine.getId()) + .name(alertDefine.getName()) + .type(alertDefine.getType()) + .expr(alertDefine.getExpr()) + .times(alertDefine.getTimes()) + .template(alertDefine.getTemplate()) + .labels(alertDefine.getLabels()) + .annotations(alertDefine.getAnnotations()) + .enable(alertDefine.isEnable()) + .build(); + MatchingLogEvent event = MatchingLogEvent.builder() + .logEntry(logEntry) + .alertDefine(define) + .eventTimestamp(System.currentTimeMillis()) + .workerTimestamp(System.currentTimeMillis()) + .build(); + WindowAggregator.WindowData clonedWindowData = new WindowAggregator.WindowData( + new WindowAggregator.WindowKey(define.getId(), System.currentTimeMillis() - 60000, System.currentTimeMillis()), + define); + clonedWindowData.addMatchingLog(event); + return clonedWindowData; + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/LogWorkerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/LogWorkerTest.java new file mode 100644 index 0000000..77d2b94 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/LogWorkerTest.java @@ -0,0 +1,239 @@ +/* + * 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.calculate.realtime.window; + +import org.apache.hertzbeat.alert.AlerterWorkerPool; +import org.apache.hertzbeat.alert.calculate.JexlExprCalculator; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.log.LogEntry; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertAll; +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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +/** + * Test case for {@link LogWorker} + */ +@ExtendWith(MockitoExtension.class) +class LogWorkerTest { + + @Mock + private AlertDefineService alertDefineService; + + @Mock + private JexlExprCalculator jexlExprCalculator; + + @Mock + private WindowAggregator windowAggregator; + + @Mock + private AlerterWorkerPool workerPool; + + @InjectMocks + private LogWorker logWorker; + + private LogEntry logEntry; + private AlertDefine alertDefine; + private List<AlertDefine> alertDefines; + + @BeforeEach + void setUp() { + // Create test log entry + logEntry = LogEntry.builder() + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .severityText("ERROR") + .body("Test error message") + .build(); + + // Create test alert define + alertDefine = AlertDefine.builder() + .id(1L) + .name("test_log_alert") + .type("realtime_log") + .expr("log.severityText == 'ERROR'") + .enable(true) + .build(); + + alertDefines = new ArrayList<>(); + alertDefines.add(alertDefine); + } + + @Test + void testReduceAndSendLogTask() { + // When + logWorker.reduceAndSendLogTask(logEntry); + + // Then + verify(workerPool).executeLogJob(any(Runnable.class)); + } + + @Test + void testReduceLogTask() { + // Given + when(alertDefineService.getLogRealTimeAlertDefines()).thenReturn(alertDefines); + when(jexlExprCalculator.execAlertExpression(anyMap(), anyString(), eq(false))).thenReturn(true); + + // When + Runnable task = logWorker.reduceLogTask(logEntry); + + // Execute the task + task.run(); + + // Then + verify(alertDefineService).getLogRealTimeAlertDefines(); + verify(jexlExprCalculator).execAlertExpression(anyMap(), eq("log.severityText == 'ERROR'"), eq(false)); + verify(windowAggregator).addMatchingLog(any(MatchingLogEvent.class)); + } + + @Test + void testProcessLogEntryWithMatchingExpression() { + // Given + when(alertDefineService.getLogRealTimeAlertDefines()).thenReturn(alertDefines); + when(jexlExprCalculator.execAlertExpression(anyMap(), anyString(), eq(false))).thenReturn(true); + + // When + Runnable task = logWorker.reduceLogTask(logEntry); + task.run(); + + // Then + @SuppressWarnings("unchecked") + ArgumentCaptor<Map<String, Object>> contextCaptor = ArgumentCaptor.forClass(Map.class); + verify(jexlExprCalculator).execAlertExpression(contextCaptor.capture(), eq("log.severityText == 'ERROR'"), eq(false)); + + Map<String, Object> context = contextCaptor.getValue(); + assertEquals(logEntry, context.get("log")); + + ArgumentCaptor<MatchingLogEvent> eventCaptor = ArgumentCaptor.forClass(MatchingLogEvent.class); + verify(windowAggregator).addMatchingLog(eventCaptor.capture()); + + MatchingLogEvent capturedEvent = eventCaptor.getValue(); + assertEquals(logEntry, capturedEvent.getLogEntry()); + assertEquals(alertDefine, capturedEvent.getAlertDefine()); + assertTrue(capturedEvent.getEventTimestamp() > 0); + assertTrue(capturedEvent.getWorkerTimestamp() > 0); + } + + @Test + void testProcessLogEntryWithNonMatchingExpression() { + // Given + when(alertDefineService.getLogRealTimeAlertDefines()).thenReturn(alertDefines); + when(jexlExprCalculator.execAlertExpression(anyMap(), anyString(), eq(false))).thenReturn(false); + + // When + Runnable task = logWorker.reduceLogTask(logEntry); + task.run(); + + // Then + verify(jexlExprCalculator).execAlertExpression(anyMap(), eq("log.severityText == 'ERROR'"), eq(false)); + verify(windowAggregator, never()).addMatchingLog(any(MatchingLogEvent.class)); + } + + @Test + void testProcessLogEntryWithMultipleAlertDefines() { + // Given + AlertDefine secondDefine = AlertDefine.builder() + .id(2L) + .name("second_alert") + .type("realtime_log") + .expr("log.body != null") + .enable(true) + .build(); + + List<AlertDefine> multipleDefines = List.of(alertDefine, secondDefine); + when(alertDefineService.getLogRealTimeAlertDefines()).thenReturn(multipleDefines); + when(jexlExprCalculator.execAlertExpression(anyMap(), eq("log.severityText == 'ERROR'"), eq(false))).thenReturn(true); + when(jexlExprCalculator.execAlertExpression(anyMap(), eq("log.body != null"), eq(false))).thenReturn(true); + + // When + Runnable task = logWorker.reduceLogTask(logEntry); + task.run(); + + // Then + verify(jexlExprCalculator).execAlertExpression(anyMap(), eq("log.severityText == 'ERROR'"), eq(false)); + verify(jexlExprCalculator).execAlertExpression(anyMap(), eq("log.body != null"), eq(false)); + verify(windowAggregator, times(2)).addMatchingLog(any(MatchingLogEvent.class)); + } + + @Test + void testCreateMatchingLogEventStructure() { + // Given + when(alertDefineService.getLogRealTimeAlertDefines()).thenReturn(alertDefines); + when(jexlExprCalculator.execAlertExpression(anyMap(), anyString(), eq(false))).thenReturn(true); + + // When + Runnable task = logWorker.reduceLogTask(logEntry); + task.run(); + + // Then + ArgumentCaptor<MatchingLogEvent> eventCaptor = ArgumentCaptor.forClass(MatchingLogEvent.class); + verify(windowAggregator).addMatchingLog(eventCaptor.capture()); + + MatchingLogEvent event = eventCaptor.getValue(); + assertAll( + () -> assertNotNull(event.getLogEntry()), + () -> assertNotNull(event.getAlertDefine()), + () -> assertTrue(event.getEventTimestamp() > 0), + () -> assertTrue(event.getWorkerTimestamp() > 0), + () -> assertEquals(logEntry, event.getLogEntry()), + () -> assertEquals(alertDefine, event.getAlertDefine()), + () -> assertTrue(event.getWorkerTimestamp() >= event.getEventTimestamp() - 1000) + ); + } + + @Test + void testLogPrefixInContext() { + // Given + when(alertDefineService.getLogRealTimeAlertDefines()).thenReturn(alertDefines); + when(jexlExprCalculator.execAlertExpression(anyMap(), anyString(), eq(false))).thenReturn(false); + + // When + Runnable task = logWorker.reduceLogTask(logEntry); + task.run(); + + // Then + @SuppressWarnings("unchecked") + ArgumentCaptor<Map<String, Object>> contextCaptor = ArgumentCaptor.forClass(Map.class); + verify(jexlExprCalculator).execAlertExpression(contextCaptor.capture(), anyString(), eq(false)); + + Map<String, Object> context = contextCaptor.getValue(); + assertTrue(context.containsKey("log")); + assertEquals(logEntry, context.get("log")); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/TimeServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/TimeServiceTest.java new file mode 100644 index 0000000..6a26924 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/TimeServiceTest.java @@ -0,0 +1,291 @@ +/* + * 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.calculate.realtime.window; + +import org.junit.jupiter.api.AfterEach; +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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test case for {@link TimeService} + */ +@ExtendWith(MockitoExtension.class) +class TimeServiceTest { + + @Mock + private TimeService.WatermarkListener mockListener1; + + @Mock + private TimeService.WatermarkListener mockListener2; + + private TimeService timeService; + + @BeforeEach + void setUp() { + timeService = new TimeService(Collections.emptyList()); + } + + @AfterEach + void tearDown() { + if (timeService != null) { + timeService.stop(); + } + } + + @Test + void testIsValidTimestamp() { + // Given + long currentTime = System.currentTimeMillis(); + long validFutureTime = currentTime + 30_000; // 30 seconds in future + long validPastTime = currentTime - 60_000; // 1 minute in past + long invalidFutureTime = currentTime + 120_000; // 2 minutes in future (exceeds 1 minute limit) + long invalidPastTime = currentTime - 25 * 60 * 60 * 1000; // 25 hours in past (exceeds 24 hours limit) + + // When & Then + assertAll( + () -> assertTrue(timeService.isValidTimestamp(currentTime)), + () -> assertTrue(timeService.isValidTimestamp(validFutureTime)), + () -> assertTrue(timeService.isValidTimestamp(validPastTime)), + () -> assertFalse(timeService.isValidTimestamp(invalidFutureTime)), + () -> assertFalse(timeService.isValidTimestamp(invalidPastTime)) + ); + } + + @Test + void testIsLateData() { + // Given + long currentTime = System.currentTimeMillis(); + timeService.updateMaxTimestamp(currentTime); + timeService.start(); + + // Wait for watermark to be set + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // When & Then + long currentWatermark = timeService.getCurrentWatermark(); + if (currentWatermark > 0) { + assertTrue(timeService.isLateData(currentWatermark - 1000)); + assertFalse(timeService.isLateData(currentWatermark + 1000)); + } + } + + @Test + void testUpdateMaxTimestamp() { + // Given + long timestamp1 = 1000L; + long timestamp2 = 2000L; + long timestamp3 = 1500L; + + // When + timeService.updateMaxTimestamp(timestamp1); + timeService.updateMaxTimestamp(timestamp2); + timeService.updateMaxTimestamp(timestamp3); + + timeService.start(); + + // Wait for potential watermark broadcast + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // Then - The max timestamp should be the highest value (timestamp2) + assertEquals(timestamp2, timeService.getMaxTimestamp()); + } + + @Test + void testWatermarkBroadcastWithRealTimestamps() throws InterruptedException { + // Given + CountDownLatch latch = new CountDownLatch(1); + AtomicLong receivedWatermark = new AtomicLong(); + + TimeService.WatermarkListener testListener = watermark -> { + receivedWatermark.set(watermark.getTimestamp()); + latch.countDown(); + }; + + timeService.addWatermarkListener(testListener); + timeService.start(); + + // When + long testTimestamp = System.currentTimeMillis(); + timeService.updateMaxTimestamp(testTimestamp); + + // Then + assertTrue(latch.await(10, TimeUnit.SECONDS), "Should receive watermark within 10 seconds"); + + long watermark = receivedWatermark.get(); + long expectedWatermark = testTimestamp - TimeService.DEFAULT_WATERMARK_DELAY_MS; // DEFAULT_WATERMARK_DELAY_MS + assertEquals(expectedWatermark, watermark); + } + + @Test + void testWatermarkMonotonicProperty() throws InterruptedException { + // Given + List<Long> receivedWatermarks = Collections.synchronizedList(new ArrayList<>()); + CountDownLatch latch = new CountDownLatch(2); + + TimeService.WatermarkListener testListener = watermark -> { + receivedWatermarks.add(watermark.getTimestamp()); + latch.countDown(); + }; + + timeService.addWatermarkListener(testListener); + timeService.start(); + + // When - Update with increasing timestamps + long baseTime = System.currentTimeMillis(); + timeService.updateMaxTimestamp(baseTime); + Thread.sleep(6000); // Wait for first watermark + + timeService.updateMaxTimestamp(baseTime + 40_000); // Increase by 40 seconds + + // Then + assertTrue(latch.await(15, TimeUnit.SECONDS), "Should receive at least 2 watermarks"); + + // Verify watermarks are monotonic (non-decreasing) + for (int i = 1; i < receivedWatermarks.size(); i++) { + assertTrue(receivedWatermarks.get(i) >= receivedWatermarks.get(i - 1), + "Watermarks should be monotonic: " + receivedWatermarks); + } + } + + @Test + void testListenerExceptionHandling() throws InterruptedException { + // Given + AtomicInteger successfulCalls = new AtomicInteger(0); + CountDownLatch latch = new CountDownLatch(1); + + // Mock listener that throws exception + TimeService.WatermarkListener faultyListener = watermark -> { + throw new RuntimeException("Test exception"); + }; + + // Mock listener that works normally + TimeService.WatermarkListener normalListener = watermark -> { + successfulCalls.incrementAndGet(); + latch.countDown(); + }; + + timeService.addWatermarkListener(faultyListener); + timeService.addWatermarkListener(normalListener); + timeService.start(); + + // When + timeService.updateMaxTimestamp(System.currentTimeMillis()); + + // Then - Normal listener should still work despite faulty listener + assertTrue(latch.await(10, TimeUnit.SECONDS), "Normal listener should receive watermark"); + assertTrue(successfulCalls.get() > 0, "Normal listener should be called successfully"); + } + + @Test + void testGetCurrentWatermark() { + // Given + timeService.start(); + + // Initially watermark should be 0 + assertEquals(0, timeService.getCurrentWatermark()); + + // When + long testTimestamp = System.currentTimeMillis(); + timeService.updateMaxTimestamp(testTimestamp); + + // Wait for watermark update + try { + Thread.sleep(6000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // Then + long watermark = timeService.getCurrentWatermark(); + assertEquals(testTimestamp - TimeService.DEFAULT_WATERMARK_DELAY_MS, watermark); + } + + @Test + void testZeroTimestampHandling() throws InterruptedException { + // Given + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger callCount = new AtomicInteger(0); + + TimeService.WatermarkListener testListener = watermark -> { + callCount.incrementAndGet(); + latch.countDown(); + }; + + timeService.addWatermarkListener(testListener); + timeService.start(); + + // When - Update with zero timestamp + timeService.updateMaxTimestamp(0); + + // Wait for potential watermark broadcast + boolean received = latch.await(8, TimeUnit.SECONDS); + + // Then - No watermark should be broadcast for zero timestamp + assertFalse(received, "No watermark should be broadcast for zero timestamp"); + assertEquals(0, callCount.get()); + } + + @Test + void testNegativeTimestampHandling() throws InterruptedException { + // Given + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger callCount = new AtomicInteger(0); + + TimeService.WatermarkListener testListener = watermark -> { + callCount.incrementAndGet(); + latch.countDown(); + }; + + timeService.addWatermarkListener(testListener); + timeService.start(); + + // When - Update with negative timestamp + timeService.updateMaxTimestamp(-1000L); + + // Wait for potential watermark broadcast + boolean received = latch.await(8, TimeUnit.SECONDS); + + // Then - No watermark should be broadcast for negative timestamp + assertFalse(received, "No watermark should be broadcast for negative timestamp"); + assertEquals(0, callCount.get()); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/WindowAggregatorTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/WindowAggregatorTest.java new file mode 100644 index 0000000..29ba10c --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/realtime/window/WindowAggregatorTest.java @@ -0,0 +1,239 @@ +/* + * 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.calculate.realtime.window; + +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +/** + * Test case for {@link WindowAggregator} + */ +@ExtendWith(MockitoExtension.class) +class WindowAggregatorTest { + + @Mock + private AlarmEvaluator alarmEvaluator; + + @InjectMocks + private WindowAggregator windowAggregator; + + private AlertDefine alertDefine; + private LogEntry logEntry; + + @BeforeEach + void setUp() { + // Create test log entry + logEntry = LogEntry.builder() + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .severityText("ERROR") + .body("Test error message") + .attributes(Map.of("service", "test-service")) + .build(); + + // Create test alert define + alertDefine = AlertDefine.builder() + .id(1L) + .name("test_log_alert") + .type("realtime_log") + .expr("log.severityText == 'ERROR'") + .period(60) // 60 seconds window + .times(1) + .enable(true) + .build(); + + // Stop the default aggregator to avoid conflicts + windowAggregator.stop(); + } + + @AfterEach + void tearDown() { + if (windowAggregator != null) { + windowAggregator.stop(); + } + } + + @Test + void testWatermarkProcessing() throws InterruptedException { + // Given - add a matching log event first + long eventTime = System.currentTimeMillis(); + long windowSize = 60 * 1000; // 60 seconds + long windowStart = (eventTime / windowSize) * windowSize; + long windowEnd = windowStart + windowSize; + + MatchingLogEvent testEvent = MatchingLogEvent.builder() + .logEntry(logEntry) + .alertDefine(alertDefine) + .eventTimestamp(eventTime) + .workerTimestamp(eventTime) + .build(); + windowAggregator.start(); + windowAggregator.addMatchingLog(testEvent); + Thread.sleep(100); // Allow processing + + // When - send watermark that should close the window + TimeService.Watermark watermark = new TimeService.Watermark(windowEnd + 1000); + windowAggregator.onWatermark(watermark); + + // Then - the window should be closed and sent to AlarmEvaluator + verify(alarmEvaluator, timeout(1000)).sendAndProcessWindowData(any(WindowAggregator.WindowData.class)); + } + + @Test + void testWatermarkNotClosingActiveWindow() throws InterruptedException { + // Given - add a matching log event + long eventTime = System.currentTimeMillis(); + MatchingLogEvent testEvent = MatchingLogEvent.builder() + .logEntry(logEntry) + .alertDefine(alertDefine) + .eventTimestamp(eventTime) + .workerTimestamp(eventTime) + .build(); + windowAggregator.start(); + windowAggregator.addMatchingLog(testEvent); + Thread.sleep(100); + + // When - send watermark that should NOT close the window (watermark before window end) + TimeService.Watermark earlyWatermark = new TimeService.Watermark(eventTime - 1000); + windowAggregator.onWatermark(earlyWatermark); + + // Give some time for potential processing + Thread.sleep(100); + + // Then - no window should be closed + verify(alarmEvaluator, never()).sendAndProcessWindowData(any(WindowAggregator.WindowData.class)); + } + + @Test + void testMultipleWindowsWithDifferentAlertDefines() throws InterruptedException { + // Given - two different alert defines + AlertDefine alertDefine2 = AlertDefine.builder() + .id(2L) + .name("second_alert") + .type("realtime_log") + .expr("log.severityText == 'WARN'") + .period(60) + .times(1) + .enable(true) + .build(); + + long eventTime = System.currentTimeMillis(); + + MatchingLogEvent event1 = MatchingLogEvent.builder() + .logEntry(logEntry) + .alertDefine(alertDefine) + .eventTimestamp(eventTime) + .workerTimestamp(eventTime) + .build(); + + MatchingLogEvent event2 = MatchingLogEvent.builder() + .logEntry(logEntry) + .alertDefine(alertDefine2) + .eventTimestamp(eventTime) + .workerTimestamp(eventTime) + .build(); + + // When + windowAggregator.start(); + windowAggregator.addMatchingLog(event1); + windowAggregator.addMatchingLog(event2); + Thread.sleep(100); + + // Send watermark to close both windows + long windowSize = 60 * 1000; + long windowStart = (eventTime / windowSize) * windowSize; + long windowEnd = windowStart + windowSize; + + TimeService.Watermark watermark = new TimeService.Watermark(windowEnd + 1000); + windowAggregator.onWatermark(watermark); + + // Then - both windows should be closed + verify(alarmEvaluator, timeout(1000).times(2)).sendAndProcessWindowData(any(WindowAggregator.WindowData.class)); + } + + @Test + void testMultipleStartCalls() { + // When - call start multiple times + windowAggregator.start(); // First call (already called in setUp) + windowAggregator.start(); // Second call + + // Then - should handle multiple start calls gracefully + assertNotNull(windowAggregator); + } + + @Test + void testWatermarkWithMultipleWindows() throws InterruptedException { + // Given - multiple windows at different times + long baseTime = System.currentTimeMillis(); + long windowSize = 60 * 1000; // 60 seconds + + // Event in first window + MatchingLogEvent event1 = MatchingLogEvent.builder() + .logEntry(logEntry) + .alertDefine(alertDefine) + .eventTimestamp(baseTime) + .workerTimestamp(baseTime) + .build(); + + // Event in second window (1 minute later) + MatchingLogEvent event2 = MatchingLogEvent.builder() + .logEntry(logEntry) + .alertDefine(alertDefine) + .eventTimestamp(baseTime + windowSize + 5000) + .workerTimestamp(baseTime + windowSize + 5000) + .build(); + windowAggregator.start(); + windowAggregator.addMatchingLog(event1); + windowAggregator.addMatchingLog(event2); + Thread.sleep(100); + + // When - send watermark that closes only the first window + long firstWindowStart = (baseTime / windowSize) * windowSize; + long firstWindowEnd = firstWindowStart + windowSize; + + TimeService.Watermark watermark = new TimeService.Watermark(firstWindowEnd + 1000); + windowAggregator.onWatermark(watermark); + + // Then - only first window should be closed + verify(alarmEvaluator, timeout(1000).times(1)).sendAndProcessWindowData(any(WindowAggregator.WindowData.class)); + + // When - send watermark that closes the second window + long secondWindowStart = ((baseTime + windowSize + 5000) / windowSize) * windowSize; + long secondWindowEnd = secondWindowStart + windowSize; + + TimeService.Watermark secondWatermark = new TimeService.Watermark(secondWindowEnd + 1000); + windowAggregator.onWatermark(secondWatermark); + + // Then - second window should also be closed (total 2 windows) + verify(alarmEvaluator, timeout(1000).times(2)).sendAndProcessWindowData(any(WindowAggregator.WindowData.class)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/config/AlertSseManagerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/config/AlertSseManagerTest.java new file mode 100644 index 0000000..40e88dd --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/config/AlertSseManagerTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +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.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +/** + * alert sse manager test + */ +public class AlertSseManagerTest { + + private AlertSseManager alertSseManager; + + @BeforeEach + void setUp() { + alertSseManager = new AlertSseManager(); + } + + @Test + void testCompleteThrowsException() throws Exception { + SseEmitter emitter = alertSseManager.createEmitter(1L); + assertNotNull(emitter); + + Map<Long, SseEmitter> emitters = new HashMap<>(); + SseEmitter spyEmitter = mock(SseEmitter.class); + + doThrow(new IllegalStateException("Simulated output stream error")).when(spyEmitter).send(any(SseEmitter.SseEventBuilder.class)); + doThrow(new RuntimeException("Complete failed")).when(spyEmitter).complete(); + + emitters.put(1L, spyEmitter); + + Field emittersField = AlertSseManager.class.getDeclaredField("emitters"); + emittersField.setAccessible(true); + emittersField.set(alertSseManager, emitters); + + assertThrows(RuntimeException.class, () -> alertSseManager.broadcast("{\"id\":1,\"content\":\"Test alert\"}")); + Map<Long, SseEmitter> currentEmitters = (Map<Long, SseEmitter>) emittersField.get(alertSseManager); + assertFalse(currentEmitters.containsKey(1L), "Emitter should still exist because complete() threw exception"); + } + +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefineControllerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefineControllerTest.java new file mode 100644 index 0000000..14471e0 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefineControllerTest.java @@ -0,0 +1,178 @@ +/* + * 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.controller; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.hertzbeat.alert.service.impl.AlertDefineServiceImpl; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.AlertDefineMonitorBind; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.support.exception.AlertExpressionException; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link AlertDefineController} + */ +@ExtendWith(MockitoExtension.class) +class AlertDefineControllerTest { + + private MockMvc mockMvc; + + private AlertDefine alertDefine; + + private List<AlertDefineMonitorBind> alertDefineMonitorBinds; + + @Mock + private AlertDefineServiceImpl alertDefineService; + + @InjectMocks + private AlertDefineController alertDefineController; + + @BeforeEach + void setUp() { + // standaloneSetup: Standalone setup, not integrated with a web environment for testing + this.mockMvc = MockMvcBuilders.standaloneSetup(alertDefineController).build(); + + this.alertDefine = AlertDefine.builder() + .id(1L) + .name("alertDefine") + .expr("1 > 0") + .times(1) + .template("template") + .creator("tom") + .modifier("tom") + .build(); + + this.alertDefineMonitorBinds = Collections.singletonList( + AlertDefineMonitorBind.builder() + .id(1L) + .alertDefineId(this.alertDefine.getId()) + .monitorId(1L) + .monitor( + Monitor.builder() + .id(1L) + .app("app") + .instance("localhost") + .name("monitor") + .build() + ) + .build() + ); + } + + @Test + void addNewAlertDefine() throws Exception { + // Simulate the client sending a request to the server + mockMvc.perform(MockMvcRequestBuilders.post("/api/alert/define") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(this.alertDefine))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void testGetDefinePreview() throws Exception { + List<Map<String, Object>> previewData = new ArrayList<>(); + Map<String, Object> row = new HashMap<>(); + row.put("__value__", 123); + row.put("job", "spring-boot"); + previewData.add(row); + + Mockito.when(alertDefineService.getDefinePreview(anyString(), anyString(), anyString())) + .thenReturn(previewData); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/alert/define/preview/{datasource}", "promql") + .param("type", "periodic") + .param("expr", "up == 1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data[0].__value__").value(123)) + .andExpect(jsonPath("$.data[0].job").value("spring-boot")); + + Mockito.when(alertDefineService.getDefinePreview(anyString(), anyString(), anyString())) + .thenThrow(new AlertExpressionException("Expression error")); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/alert/define/preview/{datasource}", "promql") + .param("type", "periodic") + .param("expr", "http_server_requests_seconds_count{!@~!!#$%^&}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").exists()) + .andExpect(jsonPath("$.msg").value("Expression error")); + + } + + @Test + void modifyAlertDefine() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.put("/api/alert/define") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(this.alertDefine))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void getAlertDefine() throws Exception { + // Simulate returning data from getAlertDefine + Mockito.when(alertDefineService.getAlertDefine(this.alertDefine.getId())) + .thenReturn(this.alertDefine); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/alert/define/" + this.alertDefine.getId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.id").value(alertDefine.getId())) + .andExpect(jsonPath("$.data.expr").value(alertDefine.getExpr())) + .andExpect(jsonPath("$.data.template").value(alertDefine.getTemplate())) + .andExpect(jsonPath("$.data.gmtCreate").value(alertDefine.getGmtCreate())) + .andExpect(jsonPath("$.data.gmtUpdate").value(alertDefine.getGmtUpdate())) + .andReturn(); + } + + @Test + void deleteAlertDefine() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.delete("/api/alert/define/" + this.alertDefine.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(this.alertDefine))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefinesControllerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefinesControllerTest.java new file mode 100644 index 0000000..f2a2579 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefinesControllerTest.java @@ -0,0 +1,144 @@ +/* + * 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.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link AlertDefinesController} + * Test whether the data mocked at the mock is correct, and test whether the format of the returned data is correct + */ +@ExtendWith(MockitoExtension.class) +class AlertDefinesControllerTest { + + private MockMvc mockMvc; + + @InjectMocks + private AlertDefinesController alertDefinesController; + + @Mock + AlertDefineService alertDefineService; + + // Parameters to avoid default values interference, default values have been replaced + List<Long> ids = Stream.of(6565463543L, 6565463544L).collect(Collectors.toList()); + Byte priority = Byte.parseByte("1"); + String sort = "gmtCreate"; + String order = "asc"; + Integer pageIndex = 1; + Integer pageSize = 7; + + // Parameter collection + Map<String, Object> content = new HashMap<>(); + + // Object for mock + PageRequest pageRequest; + + // Since the specification is used in dynamic proxy, it cannot be mocked + // Missing debugging parameters are ids, priority + // The missing part has been manually output for testing + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(alertDefinesController).build(); + content.put("ids", ids); + content.put("priority", priority); + content.put("sort", sort); + content.put("order", order); + content.put("pageIndex", pageIndex); + content.put("pageSize", pageSize); + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(content.get("order").toString()), content.get("sort").toString())); + pageRequest = PageRequest.of((Integer) content.get("pageIndex"), (Integer) content.get("pageSize"), sortExp); + } + + @Test + void getAlertDefines() throws Exception { + + // Test the correctness of the mock + // Although objects cannot be mocked, stubs can be stored using class files + // Mockito.when(alertDefineService.getAlertDefines(Mockito.any(Specification.class), Mockito.argThat(new ArgumentMatcher<PageRequest>() { + // @Override + // public boolean matches(PageRequest pageRequestMidden) { + // // There are three methods in the source code that need to be compared, namely getPageNumber(), getPageSize(), getSort() + // if(pageRequestMidden.getPageSize() == pageRequest.getPageSize() && + // pageRequestMidden.getPageNumber() == pageRequest.getPageNumber() && + // pageRequestMidden.getSort().equals(pageRequest.getSort())) { + // return true; + // } + // return false; + // } + // }))).thenReturn(new PageImpl<AlertDefine>(new ArrayList<AlertDefine>())); + AlertDefine define = AlertDefine.builder().id(9L).expr("x").times(1).build(); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, Sort.by(Sort.Order.asc(sort))); + Mockito.when(alertDefineService.getAlertDefines( + Mockito.eq(ids), + Mockito.isNull(), + Mockito.eq(sort), + Mockito.eq(order), + Mockito.eq(pageIndex), + Mockito.eq(pageSize) + )).thenReturn(new PageImpl<>(Collections.singletonList(define), pageRequest, 1)); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/alert/defines") + .param("ids", ids.toString().substring(1, ids.toString().length() - 1)) + .param("sort", sort) + .param("order", order) + .param("pageIndex", String.valueOf(pageIndex)) + .param("pageSize", String.valueOf(pageSize))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.content[0].id").value(9)) + .andExpect(jsonPath("$.data.content[0].expr").value("x")) + .andExpect(jsonPath("$.data.content[0].times").value(1)) + .andReturn(); + } + + @Test + void deleteAlertDefines() throws Exception { + this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/alert/defines") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(ids))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertGroupConvergeControllerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertGroupConvergeControllerTest.java new file mode 100644 index 0000000..9cbe8b7 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertGroupConvergeControllerTest.java @@ -0,0 +1,123 @@ +/* + * 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.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; + +import org.apache.hertzbeat.alert.service.AlertGroupConvergeService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +/** + * test case for {@link AlertGroupConvergeController} + */ +@ExtendWith(MockitoExtension.class) +public class AlertGroupConvergeControllerTest { + + private MockMvc mockMvc; + + @Mock + private AlertGroupConvergeService alertGroupConvergeService; + + private AlertGroupConverge alertGroupConverge; + + @InjectMocks + private AlertGroupConvergeController alertGroupConvergeController; + + @BeforeEach + void setUp() { + + this.mockMvc = standaloneSetup(alertGroupConvergeController).build(); + + alertGroupConverge = AlertGroupConverge.builder() + .name("test") + .creator("admin") + .modifier("admin") + .id(1L) + .build(); + } + + @Test + void testAddNewAlertGroupConverge() throws Exception { + + doNothing().when(alertGroupConvergeService).validate(any(AlertGroupConverge.class), eq(false)); + doNothing().when(alertGroupConvergeService).addAlertGroupConverge(any(AlertGroupConverge.class)); + + mockMvc.perform(post("/api/alert/group") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(alertGroupConverge))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Add success")); + } + + @Test + void testModifyAlertGroupConverge() throws Exception { + + doNothing().when(alertGroupConvergeService).validate(any(AlertGroupConverge.class), eq(true)); + doNothing().when(alertGroupConvergeService).modifyAlertGroupConverge(any(AlertGroupConverge.class)); + + mockMvc.perform(put("/api/alert/group") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(alertGroupConverge))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Modify success")); + } + + @Test + void testGetAlertGroupConvergeExists() throws Exception { + + when(alertGroupConvergeService.getAlertGroupConverge(1L)).thenReturn(alertGroupConverge); + + mockMvc.perform(get("/api/alert/group/{id}", 1L) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").value(alertGroupConverge.getId())); + } + + @Test + void testGetAlertGroupConvergeNotExists() throws Exception { + + when(alertGroupConvergeService.getAlertGroupConverge(1L)).thenReturn(null); + + mockMvc.perform(get("/api/alert/group/{id}", 1L) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.MONITOR_NOT_EXIST_CODE)) + .andExpect(jsonPath("$.msg").value("Alert Group Converge not exist.")); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertGroupConvergesControllerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertGroupConvergesControllerTest.java new file mode 100644 index 0000000..d5a9f4f --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertGroupConvergesControllerTest.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.service.AlertGroupConvergeService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + + +/** + * test case for {@link AlertGroupConvergesController} + */ + +@ExtendWith(MockitoExtension.class) +class AlertGroupConvergesControllerTest { + + private MockMvc mockMvc; + + @Mock + private AlertGroupConvergeService alertGroupConvergeService; + + @InjectMocks + private AlertGroupConvergesController alertGroupConvergesController; + + private List<AlertGroupConverge> alertGroupConvergeList; + + @BeforeEach + void setUp() { + + this.mockMvc = standaloneSetup(alertGroupConvergesController).build(); + + AlertGroupConverge alertGroupConverge1 = AlertGroupConverge.builder() + .name("Converge1") + .id(1L) + .build(); + + AlertGroupConverge alertGroupConverge2 = AlertGroupConverge.builder() + .name("Converge2") + .id(2L) + .build(); + + alertGroupConvergeList = Arrays.asList(alertGroupConverge1, alertGroupConverge2); + } + + @Test + void testGetAlertGroupConverges() throws Exception { + + Page<AlertGroupConverge> alertGroupConvergePage = new PageImpl<>( + alertGroupConvergeList, + PageRequest.of(0, 8, Sort.by("id").descending()), + alertGroupConvergeList.size() + ); + + when(alertGroupConvergeService.getAlertGroupConverges(null, null, "id", "desc", 0, 8)).thenReturn(alertGroupConvergePage); + + mockMvc.perform(get("/api/alert/groups") + .param("pageIndex", "0") + .param("pageSize", "8") + .param("sort", "id") + .param("order", "desc") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.content[0].id").value(1)) + .andExpect(jsonPath("$.data.content[0].name").value("Converge1")) + .andExpect(jsonPath("$.data.content[1].id").value(2)) + .andExpect(jsonPath("$.data.content[1].name").value("Converge2")); + } + + @Test + void testDeleteAlertDefines() throws Exception { + + doNothing().when(alertGroupConvergeService).deleteAlertGroupConverges(eq(new HashSet<>(Arrays.asList(1L, 2L)))); + + mockMvc.perform(delete("/api/alert/groups") + .param("ids", "1,2") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertInhibitControllerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertInhibitControllerTest.java new file mode 100644 index 0000000..f66d4f7 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertInhibitControllerTest.java @@ -0,0 +1,133 @@ +/* + * 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.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; + +import org.apache.hertzbeat.alert.service.AlertInhibitService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertInhibit; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.HashMap; + +/** + * test case for {@link AlertInhibitControllerTest} + */ +@ExtendWith(MockitoExtension.class) +public class AlertInhibitControllerTest { + + private MockMvc mockMvc; + + @Mock + private AlertInhibitService alertInhibitService; + + @InjectMocks + private AlertInhibitController alertInhibitController; + + private AlertInhibit alertInhibit; + + @BeforeEach + void setUp() { + this.mockMvc = standaloneSetup(alertInhibitController).build(); + + HashMap<String, String> sourceLabels = new HashMap<>(); + HashMap<String, String> targetLabels = new HashMap<>(); + + alertInhibit = AlertInhibit.builder() + .id(1L) + .name("test") + .sourceLabels(sourceLabels) + .targetLabels(targetLabels) + .creator("test") + .build(); + } + + @Test + void testAddNewAlertInhibit() throws Exception { + + doNothing().when(alertInhibitService).validate(any(AlertInhibit.class), eq(false)); + doNothing().when(alertInhibitService).addAlertInhibit(any(AlertInhibit.class)); + + mockMvc.perform(post("/api/alert/inhibit") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(alertInhibit))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Add success")); + + } + + @Test + void testModifyAlertInhibit() throws Exception { + + doNothing().when(alertInhibitService).validate(any(AlertInhibit.class), eq(true)); + doNothing().when(alertInhibitService).modifyAlertInhibit(any(AlertInhibit.class)); + + mockMvc.perform(put("/api/alert/inhibit") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(alertInhibit))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Modify success")); + + + } + + @Test + void testGetAlertInhibitExists() throws Exception { + + when(alertInhibitService.getAlertInhibit(1L)).thenReturn(alertInhibit); + + mockMvc.perform(get("/api/alert/inhibit/{id}", 1L) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").value(alertInhibit.getId())); + } + + @Test + void testGetAlertInhibitNotExists() throws Exception { + + when(alertInhibitService.getAlertInhibit(1L)).thenReturn(null); + + mockMvc.perform(get("/api/alert/inhibit/{id}", 1L) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.MONITOR_NOT_EXIST_CODE)) + .andExpect(jsonPath("$.msg").value("AlertInhibit not exist.")); + } + + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertReportControllerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertReportControllerTest.java new file mode 100644 index 0000000..a43dff4 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertReportControllerTest.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +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 java.util.HashMap; +import org.apache.hertzbeat.alert.dto.TencentCloudExternAlert; +import org.apache.hertzbeat.alert.service.ExternAlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +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.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + + +/** + * unit test for {@link AlertReportController } + */ +@Disabled +@ExtendWith(MockitoExtension.class) +class AlertReportControllerTest { + + private MockMvc mockMvc; + + @Mock + private ExternAlertService externAlertService; + + @InjectMocks + private AlertReportController alertReportController; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(alertReportController).build(); + } + + @Test + void addNewAlertReportTencent() throws Exception { + TencentCloudExternAlert.Dimensions dimensions = new TencentCloudExternAlert.Dimensions(); + dimensions.setUnInstanceId("3333"); + + TencentCloudExternAlert.AlarmObjInfo alarmObjInfo = new TencentCloudExternAlert.AlarmObjInfo(); + alarmObjInfo.setRegion("Guangzhou"); + alarmObjInfo.setNamespace("Guangzhou1"); + alarmObjInfo.setAppId("1111"); + alarmObjInfo.setUin("2222"); + alarmObjInfo.setDimensions(dimensions); + + TencentCloudExternAlert.Conditions conditions = new TencentCloudExternAlert.Conditions(); + conditions.setMetricName("xx"); + conditions.setMetricShowName("xxx"); + conditions.setCalcType("a"); + conditions.setCalcValue("aa"); + conditions.setCalcUnit("aaa"); + conditions.setCurrentValue("b"); + conditions.setCalcUnit("bb"); + conditions.setProductName("guangzhou"); + conditions.setProductShowName("Guangzhou1"); + conditions.setEventName("CVS"); + conditions.setEventShowName("Core error"); + + TencentCloudExternAlert.AlarmPolicyInfo alarmPolicyInfo = new TencentCloudExternAlert.AlarmPolicyInfo(); + alarmPolicyInfo.setPolicyTypeCname("x"); + alarmPolicyInfo.setPolicyName("Test1"); + alarmPolicyInfo.setConditions(conditions); + + TencentCloudExternAlert report = TencentCloudExternAlert.builder() + .sessionId("123") + .alarmStatus("1") + .alarmType("event") + .durationTime(2) + .firstOccurTime("2023-08-14 11:11:11") + .alarmObjInfo(alarmObjInfo) + .alarmPolicyInfo(alarmPolicyInfo) + .build(); + when(externAlertService.supportSource()).thenReturn("tencent"); + doNothing().when(externAlertService).addExternAlert(anyString()); + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/alerts/report/tencent") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(report)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void addNewAlertReport() throws Exception { + SingleAlert singleAlert = SingleAlert.builder() + .fingerprint("fingerprint") + .labels(new HashMap<>()) + .annotations(new HashMap<>()) + .content("content") + .status("firing") + .triggerTimes(1) + .startAt(1734005477630L) + .activeAt(1734005477630L) + .build(); + when(externAlertService.supportSource()).thenReturn("default"); + doNothing().when(externAlertService).addExternAlert(anyString()); + mockMvc.perform(MockMvcRequestBuilders + .post("/api/alerts/report") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(singleAlert)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertSilenceControllerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertSilenceControllerTest.java new file mode 100644 index 0000000..2ddd2f2 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertSilenceControllerTest.java @@ -0,0 +1,122 @@ +/* + * 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.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; +import org.apache.hertzbeat.alert.service.AlertSilenceService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +/** + * tes case for {@link AlertSilenceController} + */ + +@ExtendWith(MockitoExtension.class) +class AlertSilenceControllerTest { + + private MockMvc mockMvc; + + @Mock + private AlertSilenceService alertSilenceService; + + private AlertSilence alertSilence; + + @InjectMocks + private AlertSilenceController alertSilenceController; + + @BeforeEach + void setUp() { + + this.mockMvc = standaloneSetup(alertSilenceController).build(); + + alertSilence = AlertSilence.builder() + .id(1L) + .name("Test Silence") + .type((byte) 1) + .build(); + } + + @Test + void testAddNewAlertSilence() throws Exception { + + doNothing().when(alertSilenceService).validate(any(AlertSilence.class), eq(false)); + doNothing().when(alertSilenceService).addAlertSilence(any(AlertSilence.class)); + + mockMvc.perform(post("/api/alert/silence") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(alertSilence))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + + @Test + void testModifyAlertSilence() throws Exception { + + doNothing().when(alertSilenceService).validate(any(AlertSilence.class), eq(true)); + doNothing().when(alertSilenceService).modifyAlertSilence(any(AlertSilence.class)); + + mockMvc.perform(put("/api/alert/silence") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(alertSilence))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + + @Test + void testGetAlertSilence() throws Exception { + + when(alertSilenceService.getAlertSilence(1L)).thenReturn(alertSilence); + + mockMvc.perform(get("/api/alert/silence/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").value(1)) + .andExpect(jsonPath("$.data.name").value("Test Silence")); + } + + @Test + void testGetAlertSilenceNotExists() throws Exception { + + when(alertSilenceService.getAlertSilence(1L)).thenReturn(null); + + mockMvc.perform(get("/api/alert/silence/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.MONITOR_NOT_EXIST_CODE)) + .andExpect(jsonPath("$.msg").value("AlertSilence not exist.")); + } + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertsControllerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertsControllerTest.java new file mode 100644 index 0000000..318f328 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertsControllerTest.java @@ -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.alert.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.LongStream; +import org.apache.hertzbeat.alert.dto.AlertSummary; +import org.apache.hertzbeat.alert.service.AlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link AlertsController} + */ +@ExtendWith(MockitoExtension.class) +class AlertsControllerTest { + + private MockMvc mockMvc; + + @InjectMocks + private AlertsController alertsController; + + @Mock + private AlertService alertService; + + private List<Long> ids; + + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(alertsController).build(); + ids = LongStream.rangeClosed(1, 10).boxed().collect(Collectors.toList()); + } + + @Test + void getAlerts() throws Exception { + String sortField = "id"; + String orderType = "desc"; + String status = "firing"; + String content = "test"; + int pageIndex = 0; + int pageSize = 10; + + Page<GroupAlert> alertPage = new PageImpl<>( + Collections.singletonList(GroupAlert.builder().build()), + PageRequest.of(pageIndex, pageSize, Sort.by(sortField).descending()), + ids.size() + ); + Mockito.when(alertService.getGroupAlerts(status, content, sortField, orderType, pageIndex, pageSize)) + .thenReturn(alertPage); + + mockMvc.perform(MockMvcRequestBuilders + .get("/api/alerts/group") + .param("status", status) + .param("search", content) + .param("sort", sortField) + .param("order", orderType) + .param("pageIndex", String.valueOf(pageIndex)) + .param("pageSize", String.valueOf(pageSize)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.content.length()").value(1)) + .andReturn(); + } + + @Test + void deleteGroupAlerts() throws Exception { + mockMvc.perform( + MockMvcRequestBuilders + .delete("/api/alerts/group") + .param("ids", ids.stream().map(String::valueOf).collect(Collectors.joining(","))) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(content().json("{\"data\":null,\"msg\":null,\"code\":0}")) + .andReturn(); + } + + @Test + void applyGroupAlertStatus() throws Exception { + mockMvc.perform( + MockMvcRequestBuilders + .put("/api/alerts/group/status/resolved") + .param("ids", ids.stream().map(String::valueOf).collect(Collectors.joining(","))) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(content().json("{\"data\":null,\"msg\":null,\"code\":0}")) + .andReturn(); + } + + @Test + void getAlertsSummary() throws Exception { + Mockito.when(alertService.getAlertsSummary()).thenReturn(new AlertSummary()); + + mockMvc.perform( + MockMvcRequestBuilders + .get("/api/alerts/summary") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(content().json("{\"data\":{\"total\":0,\"dealNum\":0,\"rate\":0.0,\"priorityWarningNum\":0,\"priorityCriticalNum\":0,\"priorityEmergencyNum\":0},\"msg\":null,\"code\":0}")) + .andReturn(); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java new file mode 100644 index 0000000..84ca807 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java @@ -0,0 +1,508 @@ +/* + * 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.controller; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.apache.hertzbeat.alert.service.impl.NoticeConfigServiceImpl; +import org.apache.hertzbeat.common.constants.CommonConstants; +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.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link NoticeConfigController} + */ +@ExtendWith(MockitoExtension.class) +class NoticeConfigControllerTest { + + private MockMvc mockMvc; + + @Mock + private NoticeConfigServiceImpl noticeConfigService; + + @InjectMocks + private NoticeConfigController noticeConfigController; + + + public NoticeRule getNoticeRule() { + NoticeRule noticeRule = new NoticeRule(); + noticeRule.setId(87584674384L); + noticeRule.setName("dispatch-1"); + noticeRule.setReceiverId(List.of(4324324L)); + noticeRule.setReceiverName(List.of("tom")); + noticeRule.setTemplateId(4324324L); + noticeRule.setTemplateName("test"); + noticeRule.setCreator("tom"); + noticeRule.setModifier("tom"); + + return noticeRule; + } + + public NoticeReceiver getNoticeReceiver() { + + NoticeReceiver noticeReceiver = new NoticeReceiver(); + noticeReceiver.setName("tom"); + noticeReceiver.setId(5L); + noticeReceiver.setAccessToken("c03a568a306f8fd84dab51ff03cf6af6ba676a3be940c904e1df2de34853739d"); + noticeReceiver.setEmail("2762242004@qq.com"); + noticeReceiver.setHookUrl("https://www.tancloud.cn"); + noticeReceiver.setType((byte) 5); + return noticeReceiver; + + } + + public NoticeTemplate getNoticeTemplate() { + NoticeTemplate template = new NoticeTemplate(); + template.setId(5L); + template.setName("Dingding"); + template.setContent(""" + [${title}] + ${targetLabel} : ${target} + <#if (monitorId??)>${monitorIdLabel} : ${monitorId} </#if> + <#if (monitorName??)>${monitorNameLabel} : ${monitorName} </#if> + ${priorityLabel} : ${priority} + ${triggerTimeLabel} : ${triggerTime} + ${contentLabel} : ${content}"""); + template.setType((byte) 5); + + return template; + + } + + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(noticeConfigController).build(); + } + + @Test + void addNewNoticeReceiver() throws Exception { + NoticeReceiver noticeReceiver = getNoticeReceiver(); + System.out.println(noticeReceiver); + this.mockMvc.perform(post("/api/notice/receiver") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(noticeReceiver))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Add success")) + .andReturn(); + } + + @Test + void editNoticeReceiver() throws Exception { + NoticeReceiver noticeReceiver = getNoticeReceiver(); + System.out.println(noticeReceiver); + this.mockMvc.perform(put("/api/notice/receiver") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(noticeReceiver))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Edit success")) + .andReturn(); + + } + + @Test + void deleteNoticeReceiver() throws Exception { + NoticeReceiver noticeReceiver = getNoticeReceiver(); + + when(noticeConfigService.getReceiverById(7565463543L)) + .thenReturn(noticeReceiver); + when(noticeConfigService.getReceiverById(6565463543L)) + .thenReturn(null); + + + this.mockMvc.perform(delete("/api/notice/receiver/{id}", 6565463543L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("The relevant information of the recipient could not be found, please check whether the parameters are correct")) + .andReturn(); + + this.mockMvc.perform(delete("/api/notice/receiver/{id}", 7565463543L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Delete success")) + .andReturn(); + + } + + @Test + void getReceivers() throws Exception { + NoticeReceiver receiver1 = new NoticeReceiver(); + receiver1.setId(1L); + receiver1.setName("Receiver1"); + + NoticeReceiver receiver2 = new NoticeReceiver(); + receiver2.setId(2L); + receiver2.setName("Receiver2"); + + Page<NoticeReceiver> receiverPage = new PageImpl<>( + Arrays.asList(receiver1, receiver2), + PageRequest.of(0, 8, Sort.by("id").descending()), + 2 + ); + + when(noticeConfigService.getNoticeReceivers("Receiver", 0, 8)).thenReturn(receiverPage); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/receivers") + .param("name", "Receiver") + .param("pageIndex", "0") + .param("pageSize", "8") + .param("sort", "id") + .param("order", "desc") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content[0].id").value(1)) + .andExpect(jsonPath("$.data.content[0].name").value("Receiver1")) + .andExpect(jsonPath("$.data.content[1].id").value(2)) + .andExpect(jsonPath("$.data.content[1].name").value("Receiver2")) + .andExpect(jsonPath("$.data.totalElements").value(2)) + .andExpect(jsonPath("$.data.totalPages").value(1)) + .andExpect(jsonPath("$.data.size").value(8)) + .andExpect(jsonPath("$.data.number").value(0)); + } + + @Test + void getReceiverById() throws Exception { + NoticeReceiver noticeReceiver = getNoticeReceiver(); + when(noticeConfigService.getReceiverById(7565463543L)) + .thenReturn(noticeReceiver); + when(noticeConfigService.getReceiverById(6565463543L)) + .thenReturn(null); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/receiver/{id}", 6565463543L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("The relevant information of the recipient could not be found, please check whether the parameters are correct or refresh the page")) + .andReturn(); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/receiver/{id}", 7565463543L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void addNewNoticeRule() throws Exception { + NoticeRule noticeRule = getNoticeRule(); + this.mockMvc.perform(post("/api/notice/rule") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(noticeRule))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Add success")) + .andReturn(); + } + + @Test + void editNoticeRule() throws Exception { + NoticeRule noticeRule = getNoticeRule(); + this.mockMvc.perform(put("/api/notice/rule") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(noticeRule))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Edit success")) + .andReturn(); + } + + @Test + void deleteNoticeRule() throws Exception { + NoticeRule noticeRule = getNoticeRule(); + + when(noticeConfigService.getNoticeRulesById(7565463543L)) + .thenReturn(noticeRule); + when(noticeConfigService.getNoticeRulesById(6565463543L)) + .thenReturn(null); + + + this.mockMvc.perform(delete("/api/notice/rule/{id}", 6565463543L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("The specified notification rule could not be queried, please check whether the parameters are correct")) + .andReturn(); + + this.mockMvc.perform(delete("/api/notice/rule/{id}", 7565463543L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Delete success")) + .andReturn(); + } + + @Test + void getRules() throws Exception { + NoticeRule rule1 = new NoticeRule(); + rule1.setId(1L); + rule1.setName("Rule1"); + + NoticeRule rule2 = new NoticeRule(); + rule2.setId(2L); + rule2.setName("Rule2"); + + Page<NoticeRule> rulePage = new PageImpl<>( + Arrays.asList(rule1, rule2), + PageRequest.of(0, 8, Sort.by("id").descending()), + 2 + ); + + when(noticeConfigService.getNoticeRules("Rule", 0, 8)).thenReturn(rulePage); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/rules") + .param("name", "Rule") + .param("pageIndex", "0") + .param("pageSize", "8") + .param("sort", "id") + .param("order", "desc") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content[0].id").value(1)) + .andExpect(jsonPath("$.data.content[0].name").value("Rule1")) + .andExpect(jsonPath("$.data.content[1].id").value(2)) + .andExpect(jsonPath("$.data.content[1].name").value("Rule2")) + .andExpect(jsonPath("$.data.totalElements").value(2)) + .andExpect(jsonPath("$.data.totalPages").value(1)) + .andExpect(jsonPath("$.data.size").value(8)) + .andExpect(jsonPath("$.data.number").value(0)); + } + + @Test + void getRuleById() throws Exception { + NoticeRule noticeRule = getNoticeRule(); + when(noticeConfigService.getNoticeRulesById(7565463543L)) + .thenReturn(noticeRule); + when(noticeConfigService.getNoticeRulesById(6565463543L)) + .thenReturn(null); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/rule/{id}", 6565463543L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("The specified notification rule could not be queried, please check whether the parameters are correct or refresh the page")) + .andReturn(); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/rule/{id}", 7565463543L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + + @Test + void sendTestMsg() throws Exception { + NoticeReceiver noticeReceiver = getNoticeReceiver(); + when(noticeConfigService.sendTestMsg(noticeReceiver)) + .thenReturn(false); + + this.mockMvc.perform(post("/api/notice/receiver/send-test-msg") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(noticeReceiver))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("Notify service not available, please check config!")) + .andReturn(); + + + when(noticeConfigService.sendTestMsg(noticeReceiver)) + .thenReturn(true); + + this.mockMvc.perform(post("/api/notice/receiver/send-test-msg") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(noticeReceiver))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void addNewNoticeTemplate() throws Exception { + NoticeTemplate noticeTemplate = getNoticeTemplate(); + doNothing().when(noticeConfigService).addNoticeTemplate(noticeTemplate); + + this.mockMvc.perform(post("/api/notice/template") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(noticeTemplate))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Add success")) + .andReturn(); + + verify(noticeConfigService).addNoticeTemplate(noticeTemplate); + } + + @Test + void editNoticeTemplate() throws Exception { + NoticeTemplate noticeTemplate = getNoticeTemplate(); + doNothing().when(noticeConfigService).editNoticeTemplate(noticeTemplate); + + this.mockMvc.perform(put("/api/notice/template") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(noticeTemplate))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Edit success")) + .andReturn(); + + verify(noticeConfigService).editNoticeTemplate(noticeTemplate); + } + + @Test + void deleteNoticeTemplate_Success() throws Exception { + Long templateId = 1L; + when(noticeConfigService.getNoticeTemplatesById(templateId)).thenReturn(Optional.of(new NoticeTemplate())); + + mockMvc.perform(delete("/api/notice/template/{id}", templateId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Delete success")); + + Mockito.verify(noticeConfigService, Mockito.times(1)).deleteNoticeTemplate(templateId); + } + + @Test + void deleteNoticeTemplate_NotFound() throws Exception { + Long templateId = 1L; + when(noticeConfigService.getNoticeTemplatesById(templateId)).thenReturn(Optional.empty()); + + mockMvc.perform(delete("/api/notice/template/{id}", templateId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("The specified notification template could not be queried, please check whether the parameters are correct")); + + Mockito.verify(noticeConfigService, Mockito.never()).deleteNoticeTemplate(templateId); + } + + @Test + void getTemplates() throws Exception { + NoticeTemplate template1 = new NoticeTemplate(); + template1.setId(1L); + template1.setName("Template1"); + + NoticeTemplate template2 = new NoticeTemplate(); + template2.setId(2L); + template2.setName("Template2"); + + Page<NoticeTemplate> templatePage = new PageImpl<>( + Arrays.asList(template1, template2), + PageRequest.of(0, 8, Sort.by("id").descending()), + 2 + ); + + when(noticeConfigService.getNoticeTemplates("Template", true, 0, 8)).thenReturn(templatePage); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/templates") + .param("name", "Template") + .param("preset", "true") + .param("pageIndex", "0") + .param("pageSize", "8") + .param("sort", "id") + .param("order", "desc") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content[0].id").value(1)) + .andExpect(jsonPath("$.data.content[0].name").value("Template1")) + .andExpect(jsonPath("$.data.content[1].id").value(2)) + .andExpect(jsonPath("$.data.content[1].name").value("Template2")) + .andExpect(jsonPath("$.data.totalElements").value(2)) + .andExpect(jsonPath("$.data.totalPages").value(1)) + .andExpect(jsonPath("$.data.size").value(8)) + .andExpect(jsonPath("$.data.number").value(0)); + } + + @Test + void testGetTemplatesById() throws Exception { + // Mock the service response + NoticeTemplate template = new NoticeTemplate(); + template.setName("Template1"); + when(noticeConfigService.getNoticeTemplatesById(1010101010L)).thenReturn(Optional.of(template)); + when(noticeConfigService.getNoticeTemplatesById(25857585858L)).thenReturn(Optional.empty()); + // Perform the GET request and verify the response + this.mockMvc.perform(get("/api/notice/template/{id}", 1010101010L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.name").value("Template1")); + this.mockMvc.perform(get("/api/notice/template/{id}", 25857585858L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("The specified notification template could not be queried, please check whether the parameters are correct or refresh the page")); + } + + @Test + void sendTestMsg_Failure() throws Exception { + NoticeReceiver noticeReceiver = getNoticeReceiver(); + when(noticeConfigService.sendTestMsg(noticeReceiver)).thenReturn(false); + + this.mockMvc.perform(post("/api/notice/receiver/send-test-msg") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(noticeReceiver))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("Notify service not available, please check config!")) + .andReturn(); + + verify(noticeConfigService, times(1)).sendTestMsg(noticeReceiver); + } + + @Test + void getAllTemplates() throws Exception { + List<NoticeTemplate> templates = Arrays.asList(new NoticeTemplate(), new NoticeTemplate()); + when(noticeConfigService.getAllNoticeTemplates()).thenReturn(templates); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/templates/all")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void getAllReceivers() throws Exception { + List<NoticeReceiver> receivers = Arrays.asList(new NoticeReceiver(), new NoticeReceiver()); + when(noticeConfigService.getAllNoticeReceivers()).thenReturn(receivers); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/receivers/all")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/expr/AlertExpressionEvalVisitorTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/expr/AlertExpressionEvalVisitorTest.java new file mode 100644 index 0000000..c12e23e --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/expr/AlertExpressionEvalVisitorTest.java @@ -0,0 +1,873 @@ +/* + * 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.expr; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.apache.hertzbeat.warehouse.db.QueryExecutor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * test case for {@link AlertExpressionEvalVisitor} + */ +class AlertExpressionEvalVisitorTest { + + private QueryExecutor mockExecutor; + + @BeforeEach + void setUp() { + mockExecutor = Mockito.mock(QueryExecutor.class); + } + + @Test + void testGreaterThan() { + when(mockExecutor.execute("cpu")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 80.0)))); + when(mockExecutor.execute("select cpu from cpu_table where id = 1")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 80.0)))); + // promql + List<Map<String, Object>> result = evaluate("cpu > 70"); + assertEquals(1, result.size()); + assertEquals(80.0, result.get(0).get("__value__")); + //sql + result = evaluate("(select cpu from cpu_table where id = 1) > 70"); + assertEquals(1, result.size()); + assertEquals(80.0, result.get(0).get("__value__")); + } + + @Test + void testGreaterThanWithInteger() { + when(mockExecutor.execute("cpu")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 80)))); + when(mockExecutor.execute("select cpu_usage from system_metrics where host = 'server1'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 80)))); + // promql + List<Map<String, Object>> result = evaluate("cpu > 70"); + assertEquals(1, result.size()); + assertEquals(80, result.get(0).get("__value__")); + // sql + result = evaluate("(select cpu_usage from system_metrics where host = 'server1') > 70"); + assertEquals(1, result.size()); + assertEquals(80, result.get(0).get("__value__")); + } + + @Test + void testLessThan() { + when(mockExecutor.execute("memory")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 65.0)))); + when(mockExecutor.execute("select memory_usage from memory_table where instance = 'web1'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 65.0)))); + // promql + List<Map<String, Object>> result = evaluate("memory < 70"); + assertEquals(1, result.size()); + assertEquals(65.0, result.get(0).get("__value__")); + // sql + result = evaluate("(select memory_usage from memory_table where instance = 'web1') < 70"); + assertEquals(1, result.size()); + assertEquals(65.0, result.get(0).get("__value__")); + } + + @Test + void testEqualWithTolerance() { + when(mockExecutor.execute("disk")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 99.999)))); + when(mockExecutor.execute("select disk_usage from storage_metrics where partition = '/'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 99.999)))); + // promql + List<Map<String, Object>> result = evaluate("disk == 100"); + assertEquals(1, result.size()); + assertNull(result.get(0).get("__value__")); + // sql + result = evaluate("(select disk_usage from storage_metrics where partition = '/') == 100"); + assertEquals(1, result.size()); + assertNull(result.get(0).get("__value__")); + } + + @Test + void testNotEqual() { + when(mockExecutor.execute("network")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 50.0)))); + when(mockExecutor.execute("select bandwidth from network_stats where interface = 'eth0'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 50.0)))); + // promql + List<Map<String, Object>> result = evaluate("network != 60"); + assertEquals(1, result.size()); + assertEquals(50.0, result.get(0).get("__value__")); + // sql + result = evaluate("(select bandwidth from network_stats where interface = 'eth0') != 60"); + assertEquals(1, result.size()); + assertEquals(50.0, result.get(0).get("__value__")); + } + + @Test + void testExactlyEqual() { + when(mockExecutor.execute("threshold")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 100.0)))); + when(mockExecutor.execute("select alert_threshold from alert_config where rule_id = 'cpu_high'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 100.0)))); + // promql + List<Map<String, Object>> result = evaluate("threshold == 100"); + assertEquals(1, result.size()); + assertEquals(100.0, result.get(0).get("__value__")); + // sql + result = evaluate("(select alert_threshold from alert_config where rule_id = 'cpu_high') == 100"); + assertEquals(1, result.size()); + assertEquals(100.0, result.get(0).get("__value__")); + } + + @Test + void testMaxValueBoundary() { + when(mockExecutor.execute("max_val")).thenReturn(List.of(new HashMap<>(Map.of("__value__", Double.MAX_VALUE)))); + when(mockExecutor.execute("select max_value from boundary_test where test_case = 'extreme'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", Double.MAX_VALUE)))); + // promql + List<Map<String, Object>> result = evaluate("max_val > 100"); + assertEquals(1, result.size()); + assertEquals(Double.MAX_VALUE, result.get(0).get("__value__")); + // sql + result = evaluate("(select max_value from boundary_test where test_case = 'extreme') > 100"); + assertEquals(1, result.size()); + assertEquals(Double.MAX_VALUE, result.get(0).get("__value__")); + } + + @Test + void testMinValueBoundary() { + when(mockExecutor.execute("min_val")).thenReturn(List.of(new HashMap<>(Map.of("__value__", Double.MIN_VALUE)))); + when(mockExecutor.execute("select min_value from boundary_test where test_case = 'minimal'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", Double.MIN_VALUE)))); + // promql + List<Map<String, Object>> result = evaluate("min_val > 0"); + assertEquals(1, result.size()); + assertEquals(Double.MIN_VALUE, result.get(0).get("__value__")); + // sql + result = evaluate("(select min_value from boundary_test where test_case = 'minimal') > 0"); + assertEquals(1, result.size()); + assertEquals(Double.MIN_VALUE, result.get(0).get("__value__")); + } + + @Test + void testEmptyMetricName() { + when(mockExecutor.execute("")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 50.0)))); + List<Map<String, Object>> result = evaluate(" > 40"); + assertEquals(1, result.size()); + assertNull(result.get(0).get("__value__")); + } + + @Test + void testListValueWithMax() { + when(mockExecutor.execute("multi_val")).thenReturn(List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0))))); + when(mockExecutor.execute("select values from multi_metrics where group_id = 'test_group'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0))))); + // promql + List<Map<String, Object>> result = evaluate("multi_val > 25"); + assertEquals(0, result.size()); + // sql + result = evaluate("(select values from multi_metrics where group_id = 'test_group') > 25"); + assertEquals(0, result.size()); + } + + @Test + void testListValueWithMin() { + when(mockExecutor.execute("multi_val")).thenReturn(List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0))))); + when(mockExecutor.execute("select response_times from performance_data where service = 'api'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0))))); + // promql + List<Map<String, Object>> result = evaluate("multi_val < 15"); + assertEquals(0, result.size()); + // sql + result = evaluate("(select response_times from performance_data where service = 'api') < 15"); + assertEquals(0, result.size()); + } + + @Test + void testEmptyListValue() { + when(mockExecutor.execute("empty_list")).thenReturn(List.of(new HashMap<>(Map.of("__value__", List.of())))); + when(mockExecutor.execute("select error_codes from error_log where date = '2024-01-01'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", List.of())))); + // promql + List<Map<String, Object>> result = evaluate("empty_list > 50"); + assertEquals(0, result.size()); + // sql + result = evaluate("(select error_codes from error_log where date = '2024-01-01') > 50"); + assertEquals(0, result.size()); + } + + @Test + void testNestedParentheses() { + when(mockExecutor.execute("a")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 10.0)))); + when(mockExecutor.execute("b")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 20.0)))); + when(mockExecutor.execute("c")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 30.0)))); + when(mockExecutor.execute("select cpu from server_a where region = 'us-east'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 10.0)))); + when(mockExecutor.execute("select memory from server_b where region = 'us-west'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 20.0)))); + when(mockExecutor.execute("select disk from server_c where region = 'eu-central'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 30.0)))); + // promql + List<Map<String, Object>> result = evaluate("(a > 5) and (b > 15 or c < 25)"); + assertEquals(1, result.size()); + assertEquals(10.0, result.get(0).get("__value__")); + // sql + result = evaluate("((select cpu from server_a where region = 'us-east') > 5)" + + " and ((select memory from server_b where region = 'us-west') > 15" + + " or (select disk from server_c where region = 'eu-central') < 25)"); + assertEquals(1, result.size()); + assertEquals(10.0, result.get(0).get("__value__")); + } + + @Test + void testUnlessOpPromql() { + String promql = "http_server_requests_seconds_count > 10 unless http_server_requests_seconds_max > 0"; + + Map<String, Object> countValue1 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 1307); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/actuator/prometheus"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + + Map<String, Object> maxValue1 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 10.007799125); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_max"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/actuator/health"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + + when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(countValue1)); + when(mockExecutor.execute("http_server_requests_seconds_max")).thenReturn(List.of(maxValue1)); + List<Map<String, Object>> result = evaluate(promql); + assertEquals(1, result.size()); + assertEquals(1307, result.get(0).get("__value__")); + + maxValue1.put("uri", "/actuator/prometheus"); + result = evaluate(promql); + assertEquals(0, result.size()); + + Map<String, Object> sumValue1 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 20.018); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_sum"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/**"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + + maxValue1.put("uri", "/actuator/health"); + when(mockExecutor.execute("http_server_requests_seconds_sum")).thenReturn(List.of(sumValue1)); + promql = "(http_server_requests_seconds_count > 10 unless http_server_requests_seconds_max > 0) unless http_server_requests_seconds_sum > 10"; + result = evaluate(promql); + assertEquals(1, result.size()); + assertEquals(1307, result.get(0).get("__value__")); + } + + @Test + void testOrOpPromql() { + String promql = "http_server_requests_seconds_count > 10 or jvm_threads_states_threads > 0"; + + Map<String, Object> countValue1 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 1307); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/actuator/prometheus"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + + Map<String, Object> countValue2 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 16); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/**"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + + Map<String, Object> countValue3 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 7); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/actuator/health"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + + Map<String, Object> threadsValue1 = new HashMap<>() { + { + put("instance", "host.docker.internal:8989"); + put("__value__", 10.007799125); + put("__name__", "jvm_threads_states_threads"); + put("__timestamp__", "1.750320922467E9"); + put("job", "spring-boot-app"); + put("state=", "runnable"); + } + }; + + Map<String, Object> threadsValue2 = new HashMap<>() { + { + put("instance", "host.docker.internal:8989"); + put("__value__", 1); + put("__name__", "jvm_threads_states_threads"); + put("__timestamp__", "1.750320922467E9"); + put("job", "spring-boot-app"); + put("state=", "timed-waiting"); + } + }; + + Map<String, Object> threadsValue3 = new HashMap<>() { + { + put("instance", "host.docker.internal:8989"); + put("__value__", 19.02); + put("__name__", "jvm_threads_states_threads"); + put("__timestamp__", "1.750320922467E9"); + put("job", "spring-boot-app"); + put("state=", "waiting"); + } + }; + + when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(countValue1, countValue2, countValue3)); + when(mockExecutor.execute("jvm_threads_states_threads")).thenReturn(List.of(threadsValue1, threadsValue2, threadsValue3)); + List<Map<String, Object>> result = evaluate(promql); + assertEquals(5, result.size()); + assertTrue(result.stream().allMatch(t -> null != t.get("__value__"))); + + + when(mockExecutor.execute("jvm_threads_states_threads")).thenReturn(new ArrayList<>()); + result = evaluate(promql); + assertEquals(2, result.size()); + assertTrue(result.stream().allMatch(t -> null != t.get("__value__"))); + assertEquals(1307, result.get(0).get("__value__")); + + when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(new ArrayList<>()); + when(mockExecutor.execute("jvm_threads_states_threads")).thenReturn(List.of(threadsValue1, threadsValue2, threadsValue3)); + result = evaluate(promql); + assertEquals(3, result.size()); + assertTrue(result.stream().allMatch(t -> null != t.get("__value__"))); + assertEquals(10.007799125, result.get(0).get("__value__")); + } + + @Test + void testMultipleUnlessConditions() { + when(mockExecutor.execute("metric1")).thenReturn(List.of(new HashMap<>(Map.of("job", "api", "__value__", 40.0)))); + when(mockExecutor.execute("metric2")).thenReturn(List.of(new HashMap<>(Map.of("job", "web", "__value__", 50.0)))); + when(mockExecutor.execute("metric3")).thenReturn(List.of(new HashMap<>(Map.of("job", "api", "__value__", 60.0)))); + when(mockExecutor.execute("select cpu_usage from metrics where service = 'web'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 40.0)))); + when(mockExecutor.execute("select memory_usage from metrics where service = 'db'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 50.0)))); + when(mockExecutor.execute("select disk_usage from metrics where service = 'cache'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 60.0)))); + // promql + List<Map<String, Object>> result = evaluate("(metric1 > 30 unless metric2 > 45) unless metric3 > 50"); + assertEquals(0, result.size()); + // sql + result = evaluate("((select cpu_usage from metrics where service = 'web') > 30" + + " unless (select memory_usage from metrics where service = 'db') > 55)" + + " unless (select disk_usage from metrics where service = 'cache') > 50"); + assertEquals(0, result.size()); + } + + @Test + void testComplexExpressionWithMixedOperators() { + when(mockExecutor.execute("cpu_temp")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 75.0)))); + when(mockExecutor.execute("gpu_temp")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 85.0)))); + when(mockExecutor.execute("fan_speed")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 2000.0)))); + when(mockExecutor.execute("select cpu_temperature from hardware_metrics where component = 'cpu'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 75.0)))); + when(mockExecutor.execute("select gpu_temperature from hardware_metrics where component = 'gpu'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 85.0)))); + when(mockExecutor.execute("select fan_rpm from hardware_metrics where component = 'fan'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 2000.0)))); + + // promql + List<Map<String, Object>> result = evaluate("(cpu_temp > 70 and gpu_temp < 90) or fan_speed > 1500"); + assertEquals(1, result.size()); + assertEquals(75.0, result.get(0).get("__value__")); + // sql + result = evaluate("((select cpu_temperature from hardware_metrics where component = 'cpu') > 70" + + " and (select gpu_temperature from hardware_metrics where component = 'gpu') < 90)" + + " or (select fan_rpm from hardware_metrics where component = 'fan') > 1500"); + assertEquals(1, result.size()); + assertEquals(75.0, result.get(0).get("__value__")); + } + + @Test + void testSqlAggregateCount() { + when(mockExecutor.execute("select count(*) from cpu_metrics where host = 'server1'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 150)))); + List<Map<String, Object>> result = evaluate("(select count(*) from cpu_metrics where host = 'server1') > 100"); + assertEquals(1, result.size()); + assertEquals(150, result.get(0).get("__value__")); + } + + @Test + void testSqlAggregateAvg() { + when(mockExecutor.execute("select avg(cpu_usage) from system_metrics where region = 'us-east'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 75.5)))); + List<Map<String, Object>> result = evaluate("(select avg(cpu_usage) from system_metrics where region = 'us-east') > 70"); + assertEquals(1, result.size()); + assertEquals(75.5, result.get(0).get("__value__")); + } + + @Test + void testSqlAggregateSum() { + when(mockExecutor.execute("select sum(memory_used) from memory_stats where service = 'web'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 2048.0)))); + List<Map<String, Object>> result = evaluate("(select sum(memory_used) from memory_stats where service = 'web') < 3000"); + assertEquals(1, result.size()); + assertEquals(2048.0, result.get(0).get("__value__")); + } + + @Test + void testSqlAggregateMaxMin() { + when(mockExecutor.execute("select max(response_time) from api_metrics where endpoint = '/api/users'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 250.0)))); + when(mockExecutor.execute("select min(response_time) from api_metrics where endpoint = '/api/users'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 50.0)))); + + List<Map<String, Object>> result = evaluate("(select max(response_time) from api_metrics where endpoint = '/api/users') > 200"); + assertEquals(1, result.size()); + assertEquals(250.0, result.get(0).get("__value__")); + + result = evaluate("(select min(response_time) from api_metrics where endpoint = '/api/users') < 100"); + assertEquals(1, result.size()); + assertEquals(50.0, result.get(0).get("__value__")); + } + + @Test + void testSqlGroupBy() { + when(mockExecutor.execute("select avg(cpu_usage) from system_metrics where timestamp > '2024-01-01' group by host")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 80.5)))); + List<Map<String, Object>> result = evaluate("(select avg(cpu_usage) from system_metrics where timestamp > '2024-01-01' group by host) > 75"); + assertEquals(1, result.size()); + assertEquals(80.5, result.get(0).get("__value__")); + } + + @Test + void testSqlGroupByHaving() { + when(mockExecutor.execute("select count(*) from error_logs where level = 'ERROR' group by service having count(*) > 10")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 25)))); + List<Map<String, Object>> result = evaluate("(select count(*) from error_logs where level = 'ERROR' group by service having count(*) > 10) > 20"); + assertEquals(1, result.size()); + assertEquals(25, result.get(0).get("__value__")); + } + + @Test + void testSqlOrderByLimit() { + when(mockExecutor.execute("select cpu_usage from system_metrics where host like 'web%' order by cpu_usage desc limit 1")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 95.2)))); + List<Map<String, Object>> result = evaluate("(select cpu_usage from system_metrics where host like 'web%' order by cpu_usage desc limit 1) > 90"); + assertEquals(1, result.size()); + assertEquals(95.2, result.get(0).get("__value__")); + } + + @Test + void testSqlMultipleColumns() { + when(mockExecutor.execute("select avg(cpu_usage), max(memory_usage) from system_metrics where region = 'us-west'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 65.8)))); + List<Map<String, Object>> result = evaluate("(select avg(cpu_usage), max(memory_usage) from system_metrics where region = 'us-west') < 70"); + assertEquals(1, result.size()); + assertEquals(65.8, result.get(0).get("__value__")); + } + + @Test + void testSqlSubquery() { + when(mockExecutor.execute("select avg(cpu_usage) from system_metrics where host in (select host from active_servers where status = 'running')")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 72.3)))); + List<Map<String, Object>> result = evaluate("(select avg(cpu_usage) from system_metrics where host in (select host from active_servers where status = 'running')) > 70"); + assertEquals(1, result.size()); + assertEquals(72.3, result.get(0).get("__value__")); + } + + @Test + void testSqlComplexSubquery() { + when(mockExecutor.execute("select count(*) from alerts where severity = 'HIGH' and service_id in (select id from services where category = 'critical')")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 8)))); + List<Map<String, Object>> result = evaluate("(select count(*) from alerts where severity = 'HIGH' and service_id in (select id from services where category = 'critical')) >= 5"); + assertEquals(1, result.size()); + assertEquals(8, result.get(0).get("__value__")); + } + + @Test + void testSqlWithJoins() { + when(mockExecutor.execute("select avg(m.cpu_usage) from metrics m, servers s where m.server_id = s.id and s.environment = 'production'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 78.9)))); + List<Map<String, Object>> result = evaluate("(select avg(m.cpu_usage) from metrics m, servers s where m.server_id = s.id and s.environment = 'production') < 80"); + assertEquals(1, result.size()); + assertEquals(78.9, result.get(0).get("__value__")); + } + + @Test + void testSqlBetweenCondition() { + when(mockExecutor.execute("select count(*) from performance_logs where response_time between 100 and 500")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 1200)))); + List<Map<String, Object>> result = evaluate("(select count(*) from performance_logs where response_time between 100 and 500) > 1000"); + assertEquals(1, result.size()); + assertEquals(1200, result.get(0).get("__value__")); + } + + @Test + void testSqlComplexGroupByOrderBy() { + when(mockExecutor.execute("select service, avg(response_time) from api_metrics where timestamp > '2024-01-01'" + + " group by service order by avg(response_time) desc limit 5")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 180.5)))); + List<Map<String, Object>> result = evaluate("(select service, avg(response_time) from api_metrics where timestamp > '2024-01-01'" + + " group by service order by avg(response_time) desc limit 5) > 150"); + assertEquals(1, result.size()); + assertEquals(180.5, result.get(0).get("__value__")); + } + + @Test + void testSqlVarianceStddev() { + when(mockExecutor.execute("select stddev(cpu_usage) from system_metrics where host = 'db-server' and timestamp > '2024-01-01'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 12.5)))); + List<Map<String, Object>> result = evaluate("(select stddev(cpu_usage) from system_metrics where host = 'db-server' and timestamp > '2024-01-01') < 15"); + assertEquals(1, result.size()); + assertEquals(12.5, result.get(0).get("__value__")); + } + + @Test + void testSqlNestedAggregation() { + when(mockExecutor.execute("select max(daily_avg) from (select date, avg(cpu_usage) as daily_avg from system_metrics group by date) as daily_stats")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 88.7)))); + List<Map<String, Object>> result = evaluate("(select max(daily_avg) from (select date, avg(cpu_usage) as daily_avg from system_metrics group by date) as daily_stats) > 85"); + assertEquals(1, result.size()); + assertEquals(88.7, result.get(0).get("__value__")); + } + + @Test + void testSqlComplexWhereConditions() { + when(mockExecutor.execute("select count(*) from alerts where (severity = 'HIGH' or severity = 'CRITICAL') and status = 'ACTIVE' and created_at > '2024-01-01'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 42)))); + List<Map<String, Object>> result = evaluate("(select count(*) from alerts where (severity = 'HIGH' or severity = 'CRITICAL') and status = 'ACTIVE' and created_at > '2024-01-01') != 50"); + assertEquals(1, result.size()); + assertEquals(42, result.get(0).get("__value__")); + } + + @Test + void testSqlWithNullChecks() { + when(mockExecutor.execute("select count(*) from system_metrics where cpu_usage is not null and memory_usage is not null")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 9876)))); + List<Map<String, Object>> result = evaluate("(select count(*) from system_metrics where cpu_usage is not null and memory_usage is not null) > 9000"); + assertEquals(1, result.size()); + assertEquals(9876, result.get(0).get("__value__")); + } + + @Test + void testSqlMultipleAggregatesWithAlias() { + when(mockExecutor.execute("select max(cpu_usage) as max_cpu, min(cpu_usage) as min_cpu, avg(cpu_usage) as avg_cpu from system_metrics where region = 'asia'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 92.1)))); + List<Map<String, Object>> result = evaluate("(select max(cpu_usage) as max_cpu, min(cpu_usage) as min_cpu, avg(cpu_usage) as avg_cpu from system_metrics where region = 'asia') > 90"); + assertEquals(1, result.size()); + assertEquals(92.1, result.get(0).get("__value__")); + } + + @Test + void testSqlAndPromqlCallExpr() { + when(mockExecutor.execute("sum(rate(http_requests_total{job='your-service'}[1m]))")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 80)))); + when(mockExecutor.execute("select cpu_usage from system_metrics where host = 'server1'")).thenReturn( + List.of(new HashMap<>(Map.of("__value__", 80)))); + // promql + List<Map<String, Object>> result = evaluate("promql(\"sum(rate(http_requests_total{job='your-service'}[1m]))\") > 70"); + assertEquals(1, result.size()); + assertEquals(80, result.get(0).get("__value__")); + // sql + result = evaluate("sql(\"select cpu_usage from system_metrics where host = 'server1'\") > 70"); + assertEquals(1, result.size()); + assertEquals(80, result.get(0).get("__value__")); + } + + @Test + void testAndOpPromql() { + String promql = "http_server_requests_seconds_count > 10 and http_server_requests_seconds_max > 5"; + + Map<String, Object> countValue1 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 1307); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/actuator/prometheus"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + + Map<String, Object> countValue2 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 16); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/**"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + + Map<String, Object> countValue3 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 7); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/actuator/health"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + + Map<String, Object> maxValue1 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 10.007799125); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_max"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/actuator/prometheus"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + + Map<String, Object> maxValue2 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 10); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/**"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + + Map<String, Object> maxValue3 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 0); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/actuator/health"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + + when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(countValue1, countValue2, countValue3)); + when(mockExecutor.execute("http_server_requests_seconds_max")).thenReturn(List.of(maxValue1, maxValue2, maxValue3)); + List<Map<String, Object>> result = evaluate(promql); + assertEquals(2, result.size()); + assertEquals(1307, result.get(0).get("__value__")); + assertEquals(16, result.get(1).get("__value__")); + } + + @Test + void testComparisonExpr() { + String promql = "3 > bool 2"; + + // scalar and scalar + List<Map<String, Object>> result = evaluate(promql); + assertEquals(1, result.size()); + assertEquals(1, result.get(0).get("__value__")); + + // scalar and vector + promql = "0 < http_server_requests_seconds_count"; + Map<String, Object> vector = new HashMap<>() { + { + put("instance", "host.docker.internal:8989"); + put("__value__", 1307); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("uri", "/actuator/prometheus"); + put("status", "200"); + } + }; + when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(vector)); + result = evaluate(promql); + assertEquals(1, result.size()); + assertEquals(1307, result.get(0).get("__value__")); + + // scalar and vector bool + promql = "0 < bool http_server_requests_seconds_count"; + // true + result = evaluate(promql); + assertEquals(1, result.size()); + assertEquals(1, result.get(0).get("__value__")); + + // false + vector.put("__value__", -1); + result = evaluate(promql); + assertEquals(1, result.size()); + assertEquals(0, result.get(0).get("__value__")); + + // vector and scalar + promql = "http_server_requests_seconds_sum > 0"; + vector = new HashMap<>() { + { + put("instance", "host.docker.internal:8989"); + put("__value__", 13); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("uri", "/actuator/prometheus"); + put("status", "200"); + } + }; + when(mockExecutor.execute("http_server_requests_seconds_sum")).thenReturn(List.of(vector)); + result = evaluate(promql); + assertEquals(1, result.size()); + assertEquals(13, result.get(0).get("__value__")); + + // vector and scalar bool + promql = "http_server_requests_seconds_sum > bool 0"; + // true + result = evaluate(promql); + assertEquals(1, result.size()); + assertEquals(1, result.get(0).get("__value__")); + // false + vector.put("__value__", -1); + result = evaluate(promql); + assertEquals(1, result.size()); + assertEquals(0, result.get(0).get("__value__")); + + // vector and vector + promql = "http_server_requests_seconds_count > http_server_requests_seconds_sum"; + Map<String, Object> vectorCount = new HashMap<>() { + { + put("instance", "host.docker.internal:8989"); + put("__value__", 13); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("uri", "/actuator/prometheus"); + put("status", "200"); + } + }; + Map<String, Object> vectorSum = new HashMap<>() { + { + put("instance", "host.docker.internal:8989"); + put("__value__", 10); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("uri", "/actuator/prometheus"); + put("status", "200"); + } + }; + when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(vectorCount)); + when(mockExecutor.execute("http_server_requests_seconds_sum")).thenReturn(List.of(vectorSum)); + result = evaluate(promql); + assertEquals(1, result.size()); + assertEquals(13, result.get(0).get("__value__")); + + // vector and vector bool + promql = "http_server_requests_seconds_count > bool http_server_requests_seconds_sum"; + // true + result = evaluate(promql); + assertEquals(1, result.size()); + assertEquals(1, result.get(0).get("__value__")); + // false + vectorCount.put("__value__", 1); + result = evaluate(promql); + assertEquals(1, result.size()); + assertEquals(0, result.get(0).get("__value__")); + } + + private List<Map<String, Object>> evaluate(String expression) { + AlertExpressionLexer lexer = new AlertExpressionLexer(CharStreams.fromString(expression)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + AlertExpressionParser parser = new AlertExpressionParser(tokens); + return new AlertExpressionEvalVisitor(mockExecutor, tokens).visit(parser.expression()); + } +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatchTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatchTest.java new file mode 100644 index 0000000..ff4c92e --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatchTest.java @@ -0,0 +1,183 @@ +/* + * 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.notice; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyByte; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import org.apache.hertzbeat.alert.AlerterWorkerPool; +import org.apache.hertzbeat.alert.config.AlertSseManager; +import org.apache.hertzbeat.alert.service.NoticeConfigService; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.plugin.runner.PluginRunner; +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.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; + +/** + * Test case for Alert Notice Dispatch + */ +@ExtendWith(MockitoExtension.class) +class AlertNoticeDispatchTest { + + @Mock + private AlerterWorkerPool workerPool; + + @Mock + private NoticeConfigService noticeConfigService; + + @Mock + private AlertStoreHandler alertStoreHandler; + + @Mock + private PluginRunner pluginRunner; + + @Mock + private AlertNotifyHandler alertNotifyHandler; + + @Mock + private AlertSseManager emitterManager; + + private AlertNoticeDispatch alertNoticeDispatch; + + private static final int DISPATCH_THREADS = 3; + + private NoticeReceiver receiver; + private GroupAlert alert; + + @BeforeEach + void setUp() { + when(alertNotifyHandler.type()).thenReturn((byte) 1); + + List<AlertNotifyHandler> alertNotifyHandlerList = List.of(alertNotifyHandler); + alertNoticeDispatch = new AlertNoticeDispatch( + workerPool, + noticeConfigService, + alertStoreHandler, + alertNotifyHandlerList, + pluginRunner, + emitterManager + ); + + receiver = NoticeReceiver.builder() + .id(1L) + .name("test-receiver") + .type((byte) 1) + .build(); + + alert = GroupAlert.builder() + .id(1L) + .status("firing") + .alerts(Collections.singletonList(SingleAlert.builder() + .content("test-content") + .build())) + .build(); + } + + @Test + void testSendNoticeMsg() { + NoticeTemplate template = new NoticeTemplate(); + template.setId(1L); + template.setName("default-template"); + when(noticeConfigService.getDefaultNoticeTemplateByType((byte) 1)).thenReturn(template); + doNothing().when(alertNotifyHandler).send(eq(receiver), eq(template), eq(alert)); + + assertTrue(alertNoticeDispatch.sendNoticeMsg(receiver, null, alert)); + verify(alertNotifyHandler).send(eq(receiver), eq(template), eq(alert)); + } + + @Test + void testSendNoticeMsgReceiverNull() { + GroupAlert alert = new GroupAlert(); + alert.setId(1L); + alert.setStatus("firing"); + + boolean result = alertNoticeDispatch.sendNoticeMsg(null, null, alert); + assertFalse(result); + } + + @Test + void testSendNoticeMsgReceiverTypeNull() { + NoticeReceiver receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + + GroupAlert alert = new GroupAlert(); + alert.setId(1L); + alert.setStatus("firing"); + + boolean result = alertNoticeDispatch.sendNoticeMsg(receiver, null, alert); + assertFalse(result); + } + + @Test + void testSendNoticeMsgNoHandler() { + NoticeReceiver receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + receiver.setType((byte) 2); + + GroupAlert alert = new GroupAlert(); + alert.setId(1L); + alert.setStatus("firing"); + + assertFalse(alertNoticeDispatch.sendNoticeMsg(receiver, null, alert)); + } + + @Test + void testDispatchAlarmUsesTypedNotifyExecution() { + NoticeTemplate template = new NoticeTemplate(); + template.setId(1L); + template.setName("default-template"); + + when(alertStoreHandler.store(alert)).thenReturn(alert); + when(noticeConfigService.getReceiverFilterRule(alert)).thenReturn(Collections.singletonList( + org.apache.hertzbeat.common.entity.alerter.NoticeRule.builder() + .receiverId(Collections.singletonList(1L)) + .templateId(1L) + .build())); + when(noticeConfigService.getReceiverById(1L)).thenReturn(receiver); + when(noticeConfigService.getOneTemplateById(1L)).thenReturn(template); + doNothing().when(alertNotifyHandler).send(eq(receiver), eq(template), eq(alert)); + doAnswer(invocation -> { + Runnable task = invocation.getArgument(1); + task.run(); + return null; + }).when(workerPool).executeNotify(anyByte(), any(Runnable.class)); + + alertNoticeDispatch.dispatchAlarm(alert); + + verify(workerPool).executeNotify(eq((byte) 1), any(Runnable.class)); + verify(alertNotifyHandler).send(eq(receiver), eq(template), eq(alert)); + verify(emitterManager).broadcast(any(String.class)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/DbAlertStoreHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/DbAlertStoreHandlerImplTest.java new file mode 100644 index 0000000..560d650 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/DbAlertStoreHandlerImplTest.java @@ -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.notice.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.apache.hertzbeat.alert.dao.GroupAlertDao; +import org.apache.hertzbeat.alert.dao.SingleAlertDao; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import java.util.ArrayList; +import java.util.List; + +/** + * Test case for {@link DbAlertStoreHandlerImpl} + */ +@ExtendWith(MockitoExtension.class) +class DbAlertStoreHandlerImplTest { + + @Mock + private GroupAlertDao groupAlertDao; + + @Mock + private SingleAlertDao singleAlertDao; + + @InjectMocks + private DbAlertStoreHandlerImpl dbAlertStoreHandler; + + private GroupAlert groupAlert; + private SingleAlert singleAlert; + + @BeforeEach + public void setUp() { + groupAlert = new GroupAlert(); + singleAlert = new SingleAlert(); + singleAlert.setFingerprint("test-fingerprint"); + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + } + + @Test + public void testStoreEmptyAlerts() { + GroupAlert emptyGroupAlert = new GroupAlert(); + dbAlertStoreHandler.store(emptyGroupAlert); + verify(groupAlertDao, never()).save(any(GroupAlert.class)); + } + + @Test + public void testStoreNewAlert() { + String groupKey = "test-group"; + groupAlert.setGroupKey(groupKey); + + when(groupAlertDao.findByGroupKey(groupKey)).thenReturn(null); + + SingleAlert savedSingleAlert = new SingleAlert(); + savedSingleAlert.setFingerprint("test-finger"); + when(singleAlertDao.save(any(SingleAlert.class))).thenReturn(savedSingleAlert); + + GroupAlert savedGroupAlert = new GroupAlert(); + savedGroupAlert.setGroupKey(groupKey); + when(groupAlertDao.save(any(GroupAlert.class))).thenReturn(savedGroupAlert); + + dbAlertStoreHandler.store(groupAlert); + + verify(singleAlertDao).save(any(SingleAlert.class)); + verify(groupAlertDao).save(groupAlert); + } + + + @Test + public void testStoreExistingAlert() { + String groupKey = "test-group"; + String fingerprint = "test-fingerprint"; + + groupAlert.setGroupKey(groupKey); + singleAlert.setFingerprint(fingerprint); + + GroupAlert existingGroup = new GroupAlert(); + existingGroup.setId(1L); + when(groupAlertDao.findByGroupKey(groupKey)).thenReturn(existingGroup); + + SingleAlert existingAlert = new SingleAlert(); + existingAlert.setId(1L); + existingAlert.setStatus("firing"); + existingAlert.setStartAt(1000L); + existingAlert.setActiveAt(2000L); + existingAlert.setTriggerTimes(1); + when(singleAlertDao.findByFingerprint(fingerprint)).thenReturn(existingAlert); + + when(singleAlertDao.save(any(SingleAlert.class))).thenReturn(existingAlert); + when(groupAlertDao.save(any(GroupAlert.class))).thenReturn(existingGroup); + + dbAlertStoreHandler.store(groupAlert); + + verify(singleAlertDao).save(any(SingleAlert.class)); + verify(groupAlertDao).save(groupAlert); + assertEquals(1L, groupAlert.getId()); + } + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/DingTalkRobotAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/DingTalkRobotAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..d71a88c --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/DingTalkRobotAlertNotifyHandlerImplTest.java @@ -0,0 +1,135 @@ +/* + * 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.notice.impl; + +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.when; + +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Test case for DingTalk Robot Alert Notify + */ +@ExtendWith(MockitoExtension.class) +class DingTalkRobotAlertNotifyHandlerImplTest { + + @Mock + private RestTemplate restTemplate; + + @Mock + private AlerterProperties alerterProperties; + + @Mock + private ResourceBundle bundle; + + @InjectMocks + private DingTalkRobotAlertNotifyHandlerImpl dingTalkRobotAlertNotifyHandler; + + private NoticeReceiver receiver; + private GroupAlert groupAlert; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + receiver.setAccessToken("test-token"); + + groupAlert = new GroupAlert(); + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.getLabels().put("severity", "critical"); + singleAlert.getLabels().put("alertname", "Test Alert"); + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test content"); + + when(alerterProperties.getDingTalkWebhookUrl()).thenReturn("http://test.url/"); + when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + } + + @Test + public void testNotifyAlertWithInvalidToken() { + receiver.setAccessToken(null); + + assertThrows(AlertNoticeException.class, + () -> dingTalkRobotAlertNotifyHandler.send(receiver, template, groupAlert)); + } + + @Test + public void testNotifyAlertSuccess() { + CommonRobotNotifyResp successResp = new CommonRobotNotifyResp(); + successResp.setErrCode(0); + ResponseEntity<CommonRobotNotifyResp> responseEntity = + new ResponseEntity<>(successResp, HttpStatus.OK); + + when(restTemplate.postForEntity( + eq("http://test.url/test-token"), + any(), + eq(CommonRobotNotifyResp.class) + )).thenReturn(responseEntity); + + dingTalkRobotAlertNotifyHandler.send(receiver, template, groupAlert); + } + + @Test + public void testNotifyAlertFailure() { + CommonRobotNotifyResp failResp = new CommonRobotNotifyResp(); + failResp.setErrCode(1); + failResp.setErrMsg("Test Error"); + ResponseEntity<CommonRobotNotifyResp> responseEntity = + new ResponseEntity<>(failResp, HttpStatus.OK); + + when(restTemplate.postForEntity( + eq("http://test.url/test-token"), + any(), + eq(CommonRobotNotifyResp.class) + )).thenReturn(responseEntity); + + assertThrows(AlertNoticeException.class, + () -> dingTalkRobotAlertNotifyHandler.send(receiver, template, groupAlert)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/DiscordBotAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/DiscordBotAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..5033c28 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/DiscordBotAlertNotifyHandlerImplTest.java @@ -0,0 +1,131 @@ +/* + * 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.notice.impl; + +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.when; + +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Test case for Discord Bot Alert Notify + */ +@ExtendWith(MockitoExtension.class) +class DiscordBotAlertNotifyHandlerImplTest { + + @Mock + private RestTemplate restTemplate; + + @Mock + private AlerterProperties alerterProperties; + + @Mock + private ResourceBundle bundle; + + @InjectMocks + private DiscordBotAlertNotifyHandlerImpl discordBotAlertNotifyHandler; + + private NoticeReceiver receiver; + private GroupAlert groupAlert; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + receiver.setDiscordChannelId("test-channel"); + receiver.setDiscordBotToken("test-token"); + + groupAlert = new GroupAlert(); + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.getLabels().put("severity", "critical"); + singleAlert.getLabels().put("alertname", "Test Alert"); + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test content"); + + when(alerterProperties.getDiscordWebhookUrl()).thenReturn("http://test.url/channels/test-channel/messages"); + when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + } + + @Test + public void testNotifyAlertWithInvalidToken() { + receiver.setDiscordBotToken(null); + + assertThrows(AlertNoticeException.class, + () -> discordBotAlertNotifyHandler.send(receiver, template, groupAlert)); + } + + @Test + public void testNotifyAlertSuccess() { + DiscordBotAlertNotifyHandlerImpl.DiscordResponseDTO responseDTO = new DiscordBotAlertNotifyHandlerImpl.DiscordResponseDTO(); + responseDTO.setId("test-id"); + ResponseEntity<DiscordBotAlertNotifyHandlerImpl.DiscordResponseDTO> responseEntity = new ResponseEntity<>(responseDTO, HttpStatus.OK); + + when(restTemplate.postForEntity( + eq("http://test.url/channels/test-channel/messages"), + any(), + eq(DiscordBotAlertNotifyHandlerImpl.DiscordResponseDTO.class) + )).thenReturn(responseEntity); + + discordBotAlertNotifyHandler.send(receiver, template, groupAlert); + } + + @Test + public void testNotifyAlertFailure() { + ResponseEntity<String> responseEntity = new ResponseEntity<>("Error", HttpStatus.BAD_REQUEST); + + when(restTemplate.postForEntity( + eq("http://test.url/channels/test-channel/messages"), + any(), + eq(String.class) + )).thenReturn(responseEntity); + + assertThrows(AlertNoticeException.class, + () -> discordBotAlertNotifyHandler.send(receiver, template, groupAlert)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/EmailAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/EmailAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..901b27f --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/EmailAlertNotifyHandlerImplTest.java @@ -0,0 +1,133 @@ +/* + * 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.notice.impl; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Properties; +import org.apache.hertzbeat.common.entity.dto.MailServerConfig; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import jakarta.mail.internet.MimeMessage; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Test case for Email Alert Notify + */ +@ExtendWith(MockitoExtension.class) +class EmailAlertNotifyHandlerImplTest { + + @Mock + private JavaMailSenderImpl mailSender; + + @Mock + private ResourceBundle bundle; + + @Mock + private GeneralConfigDao generalConfigDao; + + @Mock + private MimeMessage mimeMessage; + + @InjectMocks + private EmailAlertNotifyHandlerImpl emailAlertNotifyHandler; + + private NoticeReceiver receiver; + private GroupAlert groupAlert; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + receiver.setEmail("test@example.com"); + + groupAlert = new GroupAlert(); + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.getLabels().put("severity", "critical"); + singleAlert.getLabels().put("alertname", "Test Alert"); + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test content"); + + // Set up email server configuration + MailServerConfig mailServerConfig = new MailServerConfig(); + mailServerConfig.setEmailHost("smtp.example.com"); + mailServerConfig.setEmailPort(587); + mailServerConfig.setEmailUsername("sender@example.com"); + mailServerConfig.setEmailPassword("password"); + mailServerConfig.setEnable(true); + + GeneralConfig generalConfig = GeneralConfig.builder() + .content(JsonUtil.toJson(mailServerConfig)) + .build(); + when(generalConfigDao.findByType(any())).thenReturn(generalConfig); + when(mailSender.getJavaMailProperties()).thenReturn(new Properties()); + } + + @Test + public void testNotifyAlertWithInvalidEmail() { + receiver.setEmail(null); + assertThrows(AlertNoticeException.class, + () -> emailAlertNotifyHandler.send(receiver, template, groupAlert)); + } + + @Test + public void testNotifyAlertSuccess() throws Exception { + when(mailSender.createMimeMessage()).thenReturn(mimeMessage); + lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + emailAlertNotifyHandler.send(receiver, template, groupAlert); + verify(mailSender).send(any(MimeMessage.class)); + } + + @Test + public void testNotifyAlertFailure() { + when(mailSender.createMimeMessage()).thenThrow(new RuntimeException("Test Error")); + assertThrows(AlertNoticeException.class, + () -> emailAlertNotifyHandler.send(receiver, template, groupAlert)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..8ce2607 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java @@ -0,0 +1,333 @@ +/* + * 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.notice.impl; + +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +/** + * Test case for FeiShu App Alert Notify + */ +@ExtendWith(MockitoExtension.class) +class FeiShuAppAlertNotifyHandlerImplTest { + + @Mock + private RestTemplate restTemplate; + + @Mock + private ResourceBundle bundle; + + @Mock + private AlerterProperties alerterProperties; + + @InjectMocks + private FeiShuAppAlertNotifyHandlerImpl feiShuAppAlertNotifyHandler; + + private NoticeReceiver receiver; + private GroupAlert groupAlert; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + receiver.setType((byte) 14); + receiver.setAppId("cli-test-app-id"); + receiver.setAppSecret("test-app-secret"); + + groupAlert = new GroupAlert(); + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.getLabels().put("severity", "critical"); + singleAlert.getLabels().put("alertname", "Test Alert"); + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test content"); + + lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + lenient().when(alerterProperties.getConsoleUrl()).thenReturn("https://console.hertzbeat.com"); + } + + /** + * Test successful notification to single user + */ + @Test + public void testNotifyAlertSuccessSingleUser() { + // Setup receiver for single user + receiver.setLarkReceiveType((byte) 0); + receiver.setUserId("user-001"); + + // Mock access token response + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp = + new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse(); + accessTokenResp.setCode(0); + accessTokenResp.setMsg("success"); + accessTokenResp.setTenantAccessToken("test-access-token"); + + // Mock message send response + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp = + new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse(); + messageResp.setCode(0); + messageResp.setMsg("success"); + + // Mock restTemplate calls + when(restTemplate.exchange( + anyString(), + eq(org.springframework.http.HttpMethod.POST), + any(), + eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class))) + .thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK)); + + when(restTemplate.exchange( + anyString(), + eq(org.springframework.http.HttpMethod.POST), + any(), + eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class))) + .thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK)); + feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert); + } + + /** + * Test successful notification to multiple users + */ + @Test + public void testNotifyAlertSuccessMultipleUsers() { + receiver.setLarkReceiveType((byte) 0); + receiver.setUserId("user-001,user-002,user-003"); + + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp = + new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse(); + accessTokenResp.setCode(0); + accessTokenResp.setMsg("success"); + accessTokenResp.setTenantAccessToken("test-access-token"); + + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp = + new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse(); + messageResp.setCode(0); + messageResp.setMsg("success"); + + when(restTemplate.exchange( + anyString(), + eq(org.springframework.http.HttpMethod.POST), + any(), + eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class))) + .thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK)); + + when(restTemplate.exchange( + anyString(), + eq(org.springframework.http.HttpMethod.POST), + any(), + eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class))) + .thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK)); + + feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert); + } + + /** + * Test successful notification to chat + */ + @Test + public void testNotifyAlertSuccessChat() { + receiver.setLarkReceiveType((byte) 1); + receiver.setChatId("chat-001"); + + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp = + new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse(); + accessTokenResp.setCode(0); + accessTokenResp.setMsg("success"); + accessTokenResp.setTenantAccessToken("test-access-token"); + + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp = + new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse(); + messageResp.setCode(0); + messageResp.setMsg("success"); + + when(restTemplate.exchange( + anyString(), + eq(org.springframework.http.HttpMethod.POST), + any(), + eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class))) + .thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK)); + + when(restTemplate.exchange( + anyString(), + eq(org.springframework.http.HttpMethod.POST), + any(), + eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class))) + .thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK)); + + feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert); + } + + /** + * Test successful notification to departments + */ + @Test + public void testNotifyAlertSuccessDepartments() { + receiver.setLarkReceiveType((byte) 2); + receiver.setPartyId("dept-001,dept-002"); + + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp = + new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse(); + accessTokenResp.setCode(0); + accessTokenResp.setMsg("success"); + accessTokenResp.setTenantAccessToken("test-access-token"); + + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp = + new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse(); + messageResp.setCode(0); + messageResp.setMsg("success"); + + when(restTemplate.exchange( + anyString(), + eq(org.springframework.http.HttpMethod.POST), + any(), + eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class))) + .thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK)); + + when(restTemplate.exchange( + anyString(), + eq(org.springframework.http.HttpMethod.POST), + any(), + eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class))) + .thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK)); + + feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert); + } + + /** + * Test notification failure due to access token error + */ + @Test + public void testNotifyAlertFailureAccessToken() { + receiver.setLarkReceiveType((byte) 0); + receiver.setUserId("user-001"); + + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp = + new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse(); + accessTokenResp.setCode(999); + accessTokenResp.setMsg("Invalid app credentials"); + + when(restTemplate.exchange( + anyString(), + eq(org.springframework.http.HttpMethod.POST), + any(), + eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class))) + .thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK)); + + assertThrows(AlertNoticeException.class, () -> { + feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert); + }); + } + + /** + * Test notification failure due to message send error + */ + @Test + public void testNotifyAlertFailureMessageSend() { + receiver.setLarkReceiveType((byte) 0); + receiver.setUserId("user-001"); + + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp = + new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse(); + accessTokenResp.setCode(0); + accessTokenResp.setMsg("success"); + accessTokenResp.setTenantAccessToken("test-access-token"); + + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp = + new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse(); + messageResp.setCode(999); + messageResp.setMsg("User not found"); + + when(restTemplate.exchange( + anyString(), + eq(org.springframework.http.HttpMethod.POST), + any(), + eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class))) + .thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK)); + + when(restTemplate.exchange( + anyString(), + eq(org.springframework.http.HttpMethod.POST), + any(), + eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class))) + .thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK)); + + assertThrows(AlertNoticeException.class, () -> { + feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert); + }); + } + + /** + * Test invalid larkReceiveType + */ + @Test + public void testInvalidLarkReceiveType() { + receiver.setLarkReceiveType((byte) 99); + receiver.setUserId("user-001"); + + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp = + new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse(); + accessTokenResp.setCode(0); + accessTokenResp.setMsg("success"); + accessTokenResp.setTenantAccessToken("test-access-token"); + + when(restTemplate.exchange( + anyString(), + eq(org.springframework.http.HttpMethod.POST), + any(), + eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class))) + .thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK)); + + assertThrows(AlertNoticeException.class, () -> { + feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert); + }); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..033de34 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImplTest.java @@ -0,0 +1,131 @@ +/* + * 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.notice.impl; + +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.when; + +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Test case for FlyBook Alert Notify + */ +@ExtendWith(MockitoExtension.class) +class FlyBookAlertNotifyHandlerImplTest { + + @Mock + private RestTemplate restTemplate; + + @Mock + private AlerterProperties alerterProperties; + + @Mock + private ResourceBundle bundle; + + @InjectMocks + private FlyBookAlertNotifyHandlerImpl flyBookAlertNotifyHandler; + + private NoticeReceiver receiver; + private GroupAlert groupAlert; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + + groupAlert = new GroupAlert(); + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.getLabels().put("severity", "critical"); + singleAlert.getLabels().put("alertname", "Test Alert"); + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test content"); + + when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + } + + @Test + public void testNotifyAlertWithInvalidToken() { + assertThrows(AlertNoticeException.class, + () -> flyBookAlertNotifyHandler.send(receiver, template, groupAlert)); + } + + @Test + public void testNotifyAlertSuccess() { + CommonRobotNotifyResp successResp = new CommonRobotNotifyResp(); + successResp.setErrCode(0); + ResponseEntity<CommonRobotNotifyResp> responseEntity = + new ResponseEntity<>(successResp, HttpStatus.OK); + + when(restTemplate.postForEntity( + any(String.class), + any(), + eq(CommonRobotNotifyResp.class) + )).thenReturn(responseEntity); + + flyBookAlertNotifyHandler.send(receiver, template, groupAlert); + } + + @Test + public void testNotifyAlertFailure() { + CommonRobotNotifyResp failResp = new CommonRobotNotifyResp(); + failResp.setCode(1); + failResp.setErrMsg("Test Error"); + ResponseEntity<CommonRobotNotifyResp> responseEntity = + new ResponseEntity<>(failResp, HttpStatus.OK); + + when(restTemplate.postForEntity( + any(String.class), + any(), + eq(CommonRobotNotifyResp.class) + )).thenReturn(responseEntity); + + assertThrows(AlertNoticeException.class, + () -> flyBookAlertNotifyHandler.send(receiver, template, groupAlert)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/GotifyAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/GotifyAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..1704f2f --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/GotifyAlertNotifyHandlerImplTest.java @@ -0,0 +1,127 @@ +/* + * 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.notice.impl; + +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.when; + +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Test case for Gotify Alert Notify + */ +@ExtendWith(MockitoExtension.class) +class GotifyAlertNotifyHandlerImplTest { + + @Mock + private RestTemplate restTemplate; + + @Mock + private AlerterProperties alerterProperties; + + @Mock + private ResourceBundle bundle; + + @InjectMocks + private GotifyAlertNotifyHandlerImpl gotifyAlertNotifyHandler; + + private NoticeReceiver receiver; + private GroupAlert groupAlert; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + receiver.setAccessToken("test-token"); + + groupAlert = new GroupAlert(); + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.getLabels().put("severity", "critical"); + singleAlert.getLabels().put("alertname", "Test Alert"); + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test content"); + + when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + when(alerterProperties.getGotifyWebhookUrl()).thenReturn("http://localhost:8080/gotify/%s"); + } + + @Test + public void testNotifyAlertSuccess() { + CommonRobotNotifyResp successResp = new CommonRobotNotifyResp(); + successResp.setErrCode(0); + ResponseEntity<CommonRobotNotifyResp> responseEntity = + new ResponseEntity<>(successResp, HttpStatus.OK); + + when(restTemplate.postForEntity( + any(String.class), + any(), + eq(CommonRobotNotifyResp.class) + )).thenReturn(responseEntity); + + gotifyAlertNotifyHandler.send(receiver, template, groupAlert); + } + + @Test + public void testNotifyAlertFailure() { + CommonRobotNotifyResp failResp = new CommonRobotNotifyResp(); + failResp.setCode(1); + failResp.setErrMsg("Test Error"); + ResponseEntity<CommonRobotNotifyResp> responseEntity = + new ResponseEntity<>(failResp, HttpStatus.BAD_REQUEST); + + when(restTemplate.postForEntity( + any(String.class), + any(), + eq(CommonRobotNotifyResp.class) + )).thenReturn(responseEntity); + + assertThrows(AlertNoticeException.class, + () -> gotifyAlertNotifyHandler.send(receiver, template, groupAlert)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/NtfyAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/NtfyAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..a64ed6c --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/NtfyAlertNotifyHandlerImplTest.java @@ -0,0 +1,288 @@ +/* + * 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.notice.impl; + +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.anyString; +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 java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +/** + * Test case for {@link NtfyAlertNotifyHandlerImpl} + */ +@ExtendWith(MockitoExtension.class) +class NtfyAlertNotifyHandlerImplTest { + + @Mock + private RestTemplate restTemplate; + + @Mock + private AlerterProperties alerterProperties; + + @Mock + private ResourceBundle bundle; + + @InjectMocks + private NtfyAlertNotifyHandlerImpl ntfyHandler; + + private NoticeReceiver receiver; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("ntfy-test"); + receiver.setNtfyServerUrl("https://ntfy.example.com"); + receiver.setNtfyTopic("hertzbeat-alerts"); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test alert content"); + + lenient().when(alerterProperties.getNtfyDefaultServerUrl()).thenReturn("https://ntfy.sh"); + lenient().when(alerterProperties.getConsoleUrl()).thenReturn("https://console.hertzbeat.com"); + lenient().when(bundle.getString("alerter.notify.title")).thenReturn("HertzBeat Alert"); + } + + @Test + void testType() { + assertEquals(15, ntfyHandler.type()); + } + + @Test + void testSendSuccess() { + GroupAlert alert = buildGroupAlert("firing", "critical"); + + ResponseEntity<String> responseEntity = new ResponseEntity<>("{\"id\":\"abc123\"}", HttpStatus.OK); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(responseEntity); + + ntfyHandler.send(receiver, template, alert); + + ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class); + verify(restTemplate).postForEntity(urlCaptor.capture(), any(HttpEntity.class), eq(String.class)); + assertEquals("https://ntfy.example.com/hertzbeat-alerts", urlCaptor.getValue()); + } + + @Test + void testSendWithAuthToken() { + receiver.setNtfyToken("tk_testtoken123"); + GroupAlert alert = buildGroupAlert("firing", "warning"); + + ResponseEntity<String> responseEntity = new ResponseEntity<>("{}", HttpStatus.OK); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(responseEntity); + + ntfyHandler.send(receiver, template, alert); + + @SuppressWarnings("unchecked") + ArgumentCaptor<HttpEntity<String>> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class); + verify(restTemplate).postForEntity(anyString(), entityCaptor.capture(), eq(String.class)); + assertEquals("Bearer tk_testtoken123", entityCaptor.getValue().getHeaders().getFirst("Authorization")); + } + + @Test + void testSendUsesDefaultServerWhenEmpty() { + receiver.setNtfyServerUrl(""); + GroupAlert alert = buildGroupAlert("firing", "info"); + + ResponseEntity<String> responseEntity = new ResponseEntity<>("{}", HttpStatus.OK); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(responseEntity); + + ntfyHandler.send(receiver, template, alert); + + ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class); + verify(restTemplate).postForEntity(urlCaptor.capture(), any(HttpEntity.class), eq(String.class)); + assertEquals("https://ntfy.sh/hertzbeat-alerts", urlCaptor.getValue()); + } + + @Test + void testSendFailure() { + GroupAlert alert = buildGroupAlert("firing", "critical"); + + ResponseEntity<String> responseEntity = new ResponseEntity<>("error", HttpStatus.INTERNAL_SERVER_ERROR); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(responseEntity); + + assertThrows(AlertNoticeException.class, () -> ntfyHandler.send(receiver, template, alert)); + } + + @Test + void testSendNetworkError() { + GroupAlert alert = buildGroupAlert("firing", "warning"); + + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenThrow(new org.springframework.web.client.ResourceAccessException("Connection refused")); + + assertThrows(AlertNoticeException.class, () -> ntfyHandler.send(receiver, template, alert)); + } + + @Test + void testMapPriorityCriticalFiring() { + GroupAlert alert = buildGroupAlert("firing", "critical"); + assertEquals(5, ntfyHandler.mapPriority(alert)); + } + + @Test + void testMapPriorityWarningFiring() { + GroupAlert alert = buildGroupAlert("firing", "warning"); + assertEquals(4, ntfyHandler.mapPriority(alert)); + } + + @Test + void testMapPriorityInfoFiring() { + GroupAlert alert = buildGroupAlert("firing", "info"); + assertEquals(3, ntfyHandler.mapPriority(alert)); + } + + @Test + void testMapPriorityResolved() { + GroupAlert alert = buildGroupAlert("resolved", "critical"); + assertEquals(2, ntfyHandler.mapPriority(alert)); + } + + @Test + void testMapPriorityUnknownSeverity() { + GroupAlert alert = buildGroupAlert("firing", null); + assertEquals(3, ntfyHandler.mapPriority(alert)); + } + + @Test + void testBuildTagsCriticalFiring() { + GroupAlert alert = buildGroupAlert("firing", "critical"); + String tags = ntfyHandler.buildTags(alert); + assertTrue(tags.contains("rotating_light")); + assertTrue(tags.contains("skull")); + } + + @Test + void testBuildTagsWarningFiring() { + GroupAlert alert = buildGroupAlert("firing", "warning"); + String tags = ntfyHandler.buildTags(alert); + assertTrue(tags.contains("warning")); + } + + @Test + void testBuildTagsResolved() { + GroupAlert alert = buildGroupAlert("resolved", "critical"); + String tags = ntfyHandler.buildTags(alert); + assertTrue(tags.contains("white_check_mark")); + } + + @Test + void testBuildTagsIncludesAlertName() { + GroupAlert alert = buildGroupAlert("firing", "warning"); + alert.getCommonLabels().put("alertname", "HighCPU"); + String tags = ntfyHandler.buildTags(alert); + assertTrue(tags.contains("HighCPU")); + } + + @Test + void testSendHeadersContainPriorityAndTags() { + GroupAlert alert = buildGroupAlert("firing", "critical"); + + ResponseEntity<String> responseEntity = new ResponseEntity<>("{}", HttpStatus.OK); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(responseEntity); + + ntfyHandler.send(receiver, template, alert); + + @SuppressWarnings("unchecked") + ArgumentCaptor<HttpEntity<String>> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class); + verify(restTemplate).postForEntity(anyString(), entityCaptor.capture(), eq(String.class)); + + HttpEntity<String> captured = entityCaptor.getValue(); + assertEquals("5", captured.getHeaders().getFirst("Priority")); + assertEquals("yes", captured.getHeaders().getFirst("Markdown")); + assertEquals("https://console.hertzbeat.com", captured.getHeaders().getFirst("Click")); + assertTrue(captured.getHeaders().getFirst("Tags").contains("rotating_light")); + } + + @Test + void testServerUrlTrailingSlashRemoved() { + receiver.setNtfyServerUrl("https://ntfy.example.com/"); + GroupAlert alert = buildGroupAlert("firing", "info"); + + ResponseEntity<String> responseEntity = new ResponseEntity<>("{}", HttpStatus.OK); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(responseEntity); + + ntfyHandler.send(receiver, template, alert); + + ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class); + verify(restTemplate).postForEntity(urlCaptor.capture(), any(HttpEntity.class), eq(String.class)); + assertEquals("https://ntfy.example.com/hertzbeat-alerts", urlCaptor.getValue()); + } + + private GroupAlert buildGroupAlert(String status, String severity) { + GroupAlert groupAlert = new GroupAlert(); + groupAlert.setStatus(status); + + Map<String, String> commonLabels = new HashMap<>(); + if (severity != null) { + commonLabels.put("severity", severity); + } + groupAlert.setCommonLabels(commonLabels); + groupAlert.setCommonAnnotations(new HashMap<>()); + groupAlert.setGroupLabels(new HashMap<>()); + + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.setAnnotations(new HashMap<>()); + if (severity != null) { + singleAlert.getLabels().put("severity", severity); + } + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + return groupAlert; + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/ServerChanAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/ServerChanAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..9ae5515 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/ServerChanAlertNotifyHandlerImplTest.java @@ -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.alert.notice.impl; + +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.lenient; +import static org.mockito.Mockito.when; + +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Test case for ServerChan Alert Notify + */ +@ExtendWith(MockitoExtension.class) +class ServerChanAlertNotifyHandlerImplTest { + + @Mock + private RestTemplate restTemplate; + + @Mock + private AlerterProperties alerterProperties; + + @Mock + private ResourceBundle bundle; + + @InjectMocks + private ServerChanAlertNotifyHandlerImpl serverChanAlertNotifyHandler; + + private NoticeReceiver receiver; + private GroupAlert groupAlert; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + receiver.setAccessToken("test-token"); + receiver.setServerChanToken("SCT193569TSNm6xIabdjqeZPtOGOWcvU1e"); + + groupAlert = new GroupAlert(); + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.getLabels().put("severity", "critical"); + singleAlert.getLabels().put("alertname", "Test Alert"); + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test content"); + + lenient().when(alerterProperties.getServerChanWebhookUrl()) + .thenReturn("https://api.serverchan.com/send/%s"); + lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + } + + @Test + public void testNotifyAlertSuccess() { + CommonRobotNotifyResp successResp = new CommonRobotNotifyResp(); + successResp.setErrCode(0); + successResp.setMsg("success"); + ResponseEntity<CommonRobotNotifyResp> responseEntity = new ResponseEntity<>(successResp, HttpStatus.OK); + + when(restTemplate.postForEntity( + any(String.class), + any(), + eq(CommonRobotNotifyResp.class))).thenReturn(responseEntity); + + serverChanAlertNotifyHandler.send(receiver, template, groupAlert); + } + + @Test + public void testNotifyAlertWithInvalidUrl() { + when(alerterProperties.getServerChanWebhookUrl()).thenReturn("http://invalid-url.com/%s"); + + assertThrows(AlertNoticeException.class, + () -> serverChanAlertNotifyHandler.send(receiver, template, groupAlert)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/SlackAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/SlackAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..6cef5a9 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/SlackAlertNotifyHandlerImplTest.java @@ -0,0 +1,107 @@ +/* + * 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.notice.impl; + +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.lenient; +import static org.mockito.Mockito.when; + +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Test case for Slack Alert Notify + */ +@ExtendWith(MockitoExtension.class) +class SlackAlertNotifyHandlerImplTest { + + @Mock + private RestTemplate restTemplate; + + @Mock + private ResourceBundle bundle; + + @InjectMocks + private SlackAlertNotifyHandlerImpl slackAlertNotifyHandler; + + private NoticeReceiver receiver; + private GroupAlert groupAlert; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + receiver.setAccessToken("test-token"); + receiver.setSlackWebHookUrl("https://hooks.slack.com/services/ABCDEF/GHIJKL/mnopqrstuvwxyz"); + + groupAlert = new GroupAlert(); + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.getLabels().put("severity", "critical"); + singleAlert.getLabels().put("alertname", "Test Alert"); + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test content"); + + lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + } + + @Test + public void testNotifyAlertSuccess() { + ResponseEntity<String> responseEntity = new ResponseEntity<>("ok", HttpStatus.OK); + + when(restTemplate.postForEntity(any(String.class), any(), eq(String.class))).thenReturn(responseEntity); + + slackAlertNotifyHandler.send(receiver, template, groupAlert); + } + + @Test + public void testNotifyAlertWithInvalidUrl() { + receiver.setSlackWebHookUrl("http://localhost:8080"); + + assertThrows(AlertNoticeException.class, + () -> slackAlertNotifyHandler.send(receiver, template, groupAlert)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/TelegramBotAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/TelegramBotAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..b18bd58 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/TelegramBotAlertNotifyHandlerImplTest.java @@ -0,0 +1,127 @@ +/* + * 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.notice.impl; + +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.lenient; +import static org.mockito.Mockito.when; + +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Test case for Telegram Bot Alert Notify + */ +@ExtendWith(MockitoExtension.class) +class TelegramBotAlertNotifyHandlerImplTest { + + @Mock + private RestTemplate restTemplate; + + @Mock + private AlerterProperties alerterProperties; + + @Mock + private ResourceBundle bundle; + + @InjectMocks + private TelegramBotAlertNotifyHandlerImpl telegramBotAlertNotifyHandler; + + private NoticeReceiver receiver; + private GroupAlert groupAlert; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + receiver.setTgBotToken("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"); + receiver.setTgUserId("123456789"); // Telegram specific - chat ID + + groupAlert = new GroupAlert(); + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.getLabels().put("severity", "critical"); + singleAlert.getLabels().put("alertname", "Test Alert"); + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test content"); + + lenient().when(alerterProperties.getTelegramWebhookUrl()) + .thenReturn("https://api.telegram.org/bot%s/sendMessage"); + lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + } + + @Test + public void testNotifyAlertSuccess() { + TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse successResp = new TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse(); + successResp.setOk(true); + successResp.setDescription("Test Success"); + + ResponseEntity<TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse> responseEntity = new ResponseEntity<>( + successResp, HttpStatus.OK); + + when(restTemplate.postForEntity(any(String.class), any(), + eq(TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse.class))).thenReturn(responseEntity); + + telegramBotAlertNotifyHandler.send(receiver, template, groupAlert); + } + + @Test + public void testNotifyAlertFailure() { + TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse failureResp = new TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse(); + failureResp.setOk(false); + failureResp.setDescription("Test failed"); + + ResponseEntity<TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse> responseEntity = new ResponseEntity<>( + failureResp, HttpStatus.OK); + + when(restTemplate.postForEntity(any(String.class), any(), + eq(TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse.class))).thenReturn(responseEntity); + + assertThrows(AlertNoticeException.class, + () -> telegramBotAlertNotifyHandler.send(receiver, template, groupAlert)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/WeComAppAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/WeComAppAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..1e750bd --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/WeComAppAlertNotifyHandlerImplTest.java @@ -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.notice.impl; + +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.lenient; +import static org.mockito.Mockito.when; + +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Test case for WeCom App Alert Notify + */ +@ExtendWith(MockitoExtension.class) +class WeComAppAlertNotifyHandlerImplTest { + + @Mock + private RestTemplate restTemplate; + + @Mock + private ResourceBundle bundle; + + @InjectMocks + private WeComAppAlertNotifyHandlerImpl weComAppAlertNotifyHandler; + + private NoticeReceiver receiver; + private GroupAlert groupAlert; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + receiver.setCorpId("test-corp-id"); + receiver.setAgentId(22); + receiver.setAppSecret("test-secret"); + + groupAlert = new GroupAlert(); + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.getLabels().put("severity", "critical"); + singleAlert.getLabels().put("alertname", "Test Alert"); + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test content"); + + lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + } + + @Test + public void testNotifyAlertSuccess() { + WeComAppAlertNotifyHandlerImpl.WeChatAppReq weChatAppReq = + new WeComAppAlertNotifyHandlerImpl.WeChatAppReq(); + weChatAppReq.setErrCode(0); + weChatAppReq.setErrMsg("ok"); + + ResponseEntity<WeComAppAlertNotifyHandlerImpl.WeChatAppReq> responseEntity = + new ResponseEntity<>(weChatAppReq, HttpStatus.OK); + + when(restTemplate.getForEntity(any(String.class), + eq(WeComAppAlertNotifyHandlerImpl.WeChatAppReq.class))) + .thenReturn(responseEntity); + + when(restTemplate.postForEntity(any(String.class), any(), + eq(WeComAppAlertNotifyHandlerImpl.WeChatAppReq.class))) + .thenReturn(responseEntity); + + weComAppAlertNotifyHandler.send(receiver, template, groupAlert); + } + + @Test + public void testNotifyAlertFailure() { + WeComAppAlertNotifyHandlerImpl.WeChatAppReq weChatAppReq = + new WeComAppAlertNotifyHandlerImpl.WeChatAppReq(); + weChatAppReq.setErrCode(2); + weChatAppReq.setErrMsg("error"); + + ResponseEntity<WeComAppAlertNotifyHandlerImpl.WeChatAppReq> responseEntity = + new ResponseEntity<>(weChatAppReq, HttpStatus.BAD_REQUEST); + + when(restTemplate.getForEntity(any(String.class), + eq(WeComAppAlertNotifyHandlerImpl.WeChatAppReq.class))) + .thenReturn(responseEntity); + + when(restTemplate.postForEntity(any(String.class), any(), + eq(WeComAppAlertNotifyHandlerImpl.WeChatAppReq.class))) + .thenReturn(responseEntity).thenReturn(responseEntity); + + assertThrows(AlertNoticeException.class, + () -> weComAppAlertNotifyHandler.send(receiver, template, groupAlert)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/WeComRobotAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/WeComRobotAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..5f81893 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/WeComRobotAlertNotifyHandlerImplTest.java @@ -0,0 +1,124 @@ +/* + * 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.notice.impl; + +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.when; + +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Test case for {@link WeComRobotAlertNotifyHandlerImpl} + */ +@ExtendWith(MockitoExtension.class) +class WeComRobotAlertNotifyHandlerImplTest { + + @Mock + private RestTemplate restTemplate; + + @Mock + private AlerterProperties alerterProperties; + + @Mock + private ResourceBundle bundle; + + @InjectMocks + private WeComRobotAlertNotifyHandlerImpl weComRobotAlertNotifyHandler; + + private NoticeReceiver receiver; + private GroupAlert groupAlert; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + receiver.setAccessToken("test-token"); + receiver.setWechatId("test-wechat-id"); + + groupAlert = new GroupAlert(); + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.getLabels().put("severity", "critical"); + singleAlert.getLabels().put("alertname", "Test Alert"); + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test content"); + + when(alerterProperties.getWeWorkWebhookUrl()).thenReturn("http://test.url/"); + when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + } + + @Test + public void testNotifyAlertSuccess() { + CommonRobotNotifyResp successResp = new CommonRobotNotifyResp(); + successResp.setErrCode(0); + ResponseEntity<CommonRobotNotifyResp> responseEntity = new ResponseEntity<>(successResp, HttpStatus.OK); + + when(restTemplate.postForEntity( + any(String.class), + any(), + eq(CommonRobotNotifyResp.class))).thenReturn(responseEntity); + + weComRobotAlertNotifyHandler.send(receiver, template, groupAlert); + } + + @Test + public void testNotifyAlertFailure() { + CommonRobotNotifyResp failResp = new CommonRobotNotifyResp(); + failResp.setCode(1); + failResp.setErrMsg("Test Error"); + ResponseEntity<CommonRobotNotifyResp> responseEntity = new ResponseEntity<>(failResp, HttpStatus.OK); + + when(restTemplate.postForEntity( + any(String.class), + any(), + eq(CommonRobotNotifyResp.class))).thenReturn(responseEntity); + + assertThrows(AlertNoticeException.class, + () -> weComRobotAlertNotifyHandler.send(receiver, template, groupAlert)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/WebHookAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/WebHookAlertNotifyHandlerImplTest.java new file mode 100644 index 0000000..e38c389 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/WebHookAlertNotifyHandlerImplTest.java @@ -0,0 +1,181 @@ +/* + * 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.notice.impl; + +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.lenient; +import static org.mockito.Mockito.when; + +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Test case for WebHook Alert Notify + */ +@ExtendWith(MockitoExtension.class) +class WebHookAlertNotifyHandlerImplTest { + + @Mock + private RestTemplate restTemplate; + + @Mock + private ResourceBundle bundle; + + @InjectMocks + private WebHookAlertNotifyHandlerImpl webHookAlertNotifyHandler; + + private NoticeReceiver receiver; + private GroupAlert groupAlert; + private NoticeTemplate template; + + @BeforeEach + public void setUp() { + receiver = new NoticeReceiver(); + receiver.setId(1L); + receiver.setName("test-receiver"); + receiver.setHookUrl("http://test.webhook.url"); + + groupAlert = new GroupAlert(); + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setLabels(new HashMap<>()); + singleAlert.getLabels().put("severity", "critical"); + singleAlert.getLabels().put("alertname", "Test Alert"); + + List<SingleAlert> alerts = new ArrayList<>(); + alerts.add(singleAlert); + groupAlert.setAlerts(alerts); + + template = new NoticeTemplate(); + template.setId(1L); + template.setName("test-template"); + template.setContent("test content"); + + lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + } + + @Test + public void testNotifyAlertSuccess() { + ResponseEntity<String> responseEntity = + new ResponseEntity<>("null", HttpStatus.OK); + + when(restTemplate.postForEntity(any(String.class), any(), eq(String.class))).thenReturn(responseEntity); + + webHookAlertNotifyHandler.send(receiver, template, groupAlert); + } + + @Test + public void testNotifyAlertFailure() { + ResponseEntity<String> responseEntity = + new ResponseEntity<>("null", HttpStatus.INTERNAL_SERVER_ERROR); + + when(restTemplate.postForEntity(any(String.class), any(), eq(String.class))).thenReturn(responseEntity); + + + assertThrows(AlertNoticeException.class, + () -> webHookAlertNotifyHandler.send(receiver, template, groupAlert)); + } + + @Test + public void testNotifyAlertWithQueryParametersSuccess() { + receiver.setHookUrl("https://prod-12.chinaeast2.logic.azure.cn:443/workflows/test/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=testSignature123"); + + ResponseEntity<String> responseEntity = + new ResponseEntity<>("null", HttpStatus.OK); + + when(restTemplate.postForEntity(eq(receiver.getHookUrl()), any(), eq(String.class))).thenReturn(responseEntity); + + webHookAlertNotifyHandler.send(receiver, template, groupAlert); + } + + @Test + public void testNotifyAlertWithNullOrEmptyUrl() { + // Test null URL + receiver.setHookUrl(null); + assertThrows(AlertNoticeException.class, + () -> webHookAlertNotifyHandler.send(receiver, template, groupAlert)); + + // Test empty URL + receiver.setHookUrl(""); + assertThrows(AlertNoticeException.class, + () -> webHookAlertNotifyHandler.send(receiver, template, groupAlert)); + + // Test blank URL + receiver.setHookUrl(" "); + assertThrows(AlertNoticeException.class, + () -> webHookAlertNotifyHandler.send(receiver, template, groupAlert)); + } + + @Test + public void testNotifyAlertWithValidUrls() { + ResponseEntity<String> responseEntity = + new ResponseEntity<>("null", HttpStatus.OK); + + when(restTemplate.postForEntity(any(String.class), any(), eq(String.class))).thenReturn(responseEntity); + + // Test various valid URLs that should work + receiver.setHookUrl("https://hooks.slack.com/services/T123/B456/complete-token"); + webHookAlertNotifyHandler.send(receiver, template, groupAlert); + + receiver.setHookUrl("https://discord.com/api/webhooks/123456789/complete-token"); + webHookAlertNotifyHandler.send(receiver, template, groupAlert); + + receiver.setHookUrl("https://example.com/webhook?complete=true&valid=yes"); + webHookAlertNotifyHandler.send(receiver, template, groupAlert); + } + + @Test + public void testNotifyAlertWithLongUrlSuccess() { + // Test that long URLs (over 300 chars) can now be handled with the increased field length + StringBuilder longUrl = new StringBuilder("https://prod-12.chinaeast2.logic.azure.cn:443/workflows/test/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig="); + for (int i = 0; i < 200; i++) { + longUrl.append("x"); + } + receiver.setHookUrl(longUrl.toString()); + + ResponseEntity<String> responseEntity = + new ResponseEntity<>("null", HttpStatus.OK); + + when(restTemplate.postForEntity(eq(receiver.getHookUrl()), any(), eq(String.class))).thenReturn(responseEntity); + + webHookAlertNotifyHandler.send(receiver, template, groupAlert); + + // Verify the URL is longer than the old 300 char limit + assertTrue(receiver.getHookUrl().length() > 300, + "URL should be longer than old 300 char limit to test the field length increase"); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java new file mode 100644 index 0000000..0208188 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java @@ -0,0 +1,138 @@ +/* + * 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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.junit.jupiter.api.AfterEach; +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; + +/** + * test case for {@link AlarmCommonReduce} + */ + +@ExtendWith(MockitoExtension.class) +class AlarmCommonReduceTest { + + @Mock + private AlarmGroupReduce alarmGroupReduce; + + private AlarmCommonReduce alarmCommonReduce; + + private SingleAlert testAlert; + + @BeforeEach + void setUp() { + testAlert = SingleAlert.builder().labels(new HashMap<>(Map.of("alertname", "test"))).build(); + alarmCommonReduce = new AlarmCommonReduce(alarmGroupReduce); + } + + @AfterEach + void tearDown() { + if (alarmCommonReduce != null) { + alarmCommonReduce.destroy(); + } + } + + @Test + void testReduceAndSendAlarm() { + alarmCommonReduce.reduceAndSendAlarm(testAlert); + } + + @Test + void testReduceAndSendAlarmRunsOnVirtualThread() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + doAnswer(invocation -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + return null; + }).when(alarmGroupReduce).processGroupAlert(any(SingleAlert.class)); + + alarmCommonReduce.reduceAndSendAlarm(testAlert); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void testReduceAndSendAlarmQueuesWhenConcurrencyLimitReached() throws Exception { + VirtualThreadProperties properties = new VirtualThreadProperties( + true, + VirtualThreadProperties.PoolProperties.collectorDefaults(), + VirtualThreadProperties.PoolProperties.commonDefaults(), + VirtualThreadProperties.PoolProperties.managerDefaults(), + new VirtualThreadProperties.AlerterProperties( + VirtualThreadProperties.PoolProperties.alerterNotifyDefaults(), + 10, + VirtualThreadProperties.QueueProperties.logWorkerDefaults(), + new VirtualThreadProperties.QueueProperties(1, 0), + VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(), + 4), + VirtualThreadProperties.PoolProperties.warehouseDefaults(), + VirtualThreadProperties.AsyncProperties.defaults()); + alarmCommonReduce.destroy(); + alarmCommonReduce = new AlarmCommonReduce(alarmGroupReduce, properties); + + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + AtomicInteger invocationOrder = new AtomicInteger(); + doAnswer(invocation -> { + int order = invocationOrder.incrementAndGet(); + if (order == 1) { + firstStarted.countDown(); + try { + releaseFirst.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } else if (order == 2) { + secondStarted.countDown(); + } + return null; + }).when(alarmGroupReduce).processGroupAlert(any(SingleAlert.class)); + + alarmCommonReduce.reduceAndSendAlarm(SingleAlert.builder() + .labels(new HashMap<>(Map.of("name", "first"))).build()); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + alarmCommonReduce.reduceAndSendAlarm(SingleAlert.builder() + .labels(new HashMap<>(Map.of("name", "second"))).build()); + assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS)); + + releaseFirst.countDown(); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + } + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduceTest.java new file mode 100644 index 0000000..a042032 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduceTest.java @@ -0,0 +1,238 @@ +/* + * 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 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; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test for AlarmGroupReduce + */ +class AlarmGroupReduceTest { + + @Mock + private AlarmInhibitReduce alarmInhibitReduce; + + @Mock + private AlertGroupConvergeDao alertGroupConvergeDao; + + private AlarmGroupReduce alarmGroupReduce; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + when(alertGroupConvergeDao.findAlertGroupConvergesByEnableIsTrue()) + .thenReturn(Collections.emptyList()); + alarmGroupReduce = new AlarmGroupReduce(alarmInhibitReduce, alertGroupConvergeDao, + new VirtualThreadProperties(), false); + } + + @AfterEach + void tearDown() { + if (alarmGroupReduce != null) { + alarmGroupReduce.destroy(); + } + } + + @Test + void whenNoGroupRules_shouldSendSingleAlert() { + SingleAlert alert = SingleAlert.builder() + .fingerprint("fp1") + .status("firing") + .labels(createLabels("severity", "critical")) + .build(); + + alarmGroupReduce.processGroupAlert(alert); + + verify(alarmInhibitReduce).inhibitAlarm(argThat(group -> + group.getAlerts().size() == 1 && group.getAlerts().get(0).getFingerprint().equals("fp1"))); + } + + @Test + void whenMatchingGroupRule_shouldGroup() { + // Setup group rule + AlertGroupConverge rule = new AlertGroupConverge(); + rule.setName("test-rule"); + rule.setGroupLabels(Arrays.asList("severity", "instance")); + when(alertGroupConvergeDao.findAlertGroupConvergesByEnableIsTrue()) + .thenReturn(Collections.singletonList(rule)); + alarmGroupReduce.refreshGroupDefines(Collections.singletonList(rule)); + + SingleAlert alert = SingleAlert.builder() + .fingerprint("fp1") + .status("firing") + .labels(createLabels("severity", "critical", "instance", "host1")) + .build(); + + alarmGroupReduce.processGroupAlert(alert); + + // Verify group is created and cached (implicitly tested through internal state) + verify(alarmInhibitReduce, never()).inhibitAlarm(any()); // Should not send immediately due to group wait + } + + @Test + void dispatchCheckAndSendGroupsRunsOnVirtualThread() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + alarmGroupReduce.destroy(); + alarmGroupReduce = new TestAlarmGroupReduce(alarmInhibitReduce, alertGroupConvergeDao, + new VirtualThreadProperties(), latch, virtualThread, null, null, null, null, null); + + alarmGroupReduce.dispatchCheckAndSendGroups(); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void dispatchCheckAndSendGroupsDoesNotRunConcurrently() throws Exception { + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + AtomicInteger maxConcurrent = new AtomicInteger(); + alarmGroupReduce.destroy(); + alarmGroupReduce = new TestAlarmGroupReduce(alarmInhibitReduce, alertGroupConvergeDao, + new VirtualThreadProperties(), null, null, firstStarted, releaseFirst, secondStarted, + maxConcurrent, new AtomicInteger()); + + alarmGroupReduce.dispatchCheckAndSendGroups(); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + alarmGroupReduce.dispatchCheckAndSendGroups(); + assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS)); + + releaseFirst.countDown(); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + assertEquals(1, maxConcurrent.get()); + } + + private Map<String, String> createLabels(String... keyValues) { + Map<String, String> labels = new HashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + labels.put(keyValues[i], keyValues[i + 1]); + } + return labels; + } + + private static final class TestAlarmGroupReduce extends AlarmGroupReduce { + + private final CountDownLatch virtualThreadLatch; + + private final AtomicBoolean virtualThread; + + private final CountDownLatch firstStarted; + + private final CountDownLatch releaseFirst; + + private final CountDownLatch secondStarted; + + private final AtomicInteger maxConcurrent; + + private final AtomicInteger concurrent; + + private final AtomicInteger invocations; + + private TestAlarmGroupReduce(AlarmInhibitReduce alarmInhibitReduce, AlertGroupConvergeDao alertGroupConvergeDao, + VirtualThreadProperties properties, CountDownLatch virtualThreadLatch, + AtomicBoolean virtualThread, CountDownLatch firstStarted, + CountDownLatch releaseFirst, CountDownLatch secondStarted, + AtomicInteger maxConcurrent, AtomicInteger invocations) { + super(alarmInhibitReduce, alertGroupConvergeDao, properties, false); + this.virtualThreadLatch = virtualThreadLatch; + this.virtualThread = virtualThread; + this.firstStarted = firstStarted; + this.releaseFirst = releaseFirst; + this.secondStarted = secondStarted; + this.maxConcurrent = maxConcurrent; + this.invocations = invocations; + this.concurrent = maxConcurrent == null ? null : new AtomicInteger(); + } + + @Override + void beforeCheckAndSendGroupsRun() { + if (virtualThread != null) { + virtualThread.set(Thread.currentThread().isVirtual()); + } + if (virtualThreadLatch != null) { + virtualThreadLatch.countDown(); + } + if (maxConcurrent == null || invocations == null) { + return; + } + int running = concurrent.incrementAndGet(); + maxConcurrent.accumulateAndGet(running, Math::max); + int currentInvocation = invocations.incrementAndGet(); + try { + if (currentInvocation == 1 && firstStarted != null && releaseFirst != null) { + firstStarted.countDown(); + releaseFirst.await(5, TimeUnit.SECONDS); + } else if (currentInvocation == 2 && secondStarted != null) { + secondStarted.countDown(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + concurrent.decrementAndGet(); + } + } + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduceTest.java new file mode 100644 index 0000000..7b264df --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmInhibitReduceTest.java @@ -0,0 +1,466 @@ +/* + * 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 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; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.hertzbeat.alert.AlerterProperties; +import org.apache.hertzbeat.alert.dao.AlertInhibitDao; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.common.entity.alerter.AlertInhibit; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test for AlarmInhibitReduce + */ +class AlarmInhibitReduceTest { + + @Mock + private AlertInhibitDao alertInhibitDao; + + @Mock + private AlarmSilenceReduce alarmSilenceReduce; + + @Mock + private AlerterProperties alerterProperties; + + private AlarmInhibitReduce alarmInhibitReduce; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + when(alertInhibitDao.findAlertInhibitsByEnableIsTrue()) + .thenReturn(Collections.emptyList()); + + // Correctly set up AlerterProperties mock + AlerterProperties.InhibitProperties inhibitProperties = new AlerterProperties.InhibitProperties(); + inhibitProperties.setTtl(60000); + when(alerterProperties.getInhibit()).thenReturn(inhibitProperties); + + alarmInhibitReduce = new AlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties, + new VirtualThreadProperties(), false); + } + + @AfterEach + void tearDown() { + if (alarmInhibitReduce != null) { + alarmInhibitReduce.destroy(); + } + } + + @Test + void whenNoInhibitRules_shouldForwardAlert() { + SingleAlert alert = createSingleAlert("firing", "fp1", + createLabels("severity", "warning")); + GroupAlert groupAlert = createGroupAlert("firing", + createLabels("severity", "warning"), + Stream.of(alert).collect(Collectors.toList())); + + alarmInhibitReduce.inhibitAlarm(groupAlert); + + verify(alarmSilenceReduce).silenceAlarm(groupAlert); + } + + @Test + void whenSourceAlertMatches_shouldInhibitTargetAlert() { + AlertInhibit rule = AlertInhibit.builder() + .id(1L) + .enable(true) + .sourceLabels(createLabels("severity", "critical")) + .targetLabels(createLabels("severity", "warning")) + .equalLabels(Collections.singletonList("instance")) + .build(); + + alarmInhibitReduce.refreshInhibitRules(Collections.singletonList(rule)); + + // Create and process source alert + SingleAlert sourceAlert = createSingleAlert("firing", "fp1", + createLabels("severity", "critical", "instance", "host1")); + GroupAlert sourceGroupAlert = createGroupAlert("firing", + createLabels("severity", "critical", "instance", "host1"), + Stream.of(sourceAlert).collect(Collectors.toList())); + alarmInhibitReduce.inhibitAlarm(sourceGroupAlert); + + // Create and process target alert + SingleAlert targetAlert = createSingleAlert("firing", "fp2", + createLabels("severity", "warning", "instance", "host1")); + GroupAlert targetGroupAlert = createGroupAlert("firing", + createLabels("severity", "warning", "instance", "host1"), + Stream.of(targetAlert).collect(Collectors.toList())); + alarmInhibitReduce.inhibitAlarm(targetGroupAlert); + + verify(alarmSilenceReduce).silenceAlarm(sourceGroupAlert); + verify(alarmSilenceReduce, never()).silenceAlarm(targetGroupAlert); + } + + @Test + void whenSourceAlertDoesNotMatch_shouldNotInhibitTargetAlert() { + AlertInhibit rule = AlertInhibit.builder() + .id(1L) + .enable(true) + .sourceLabels(createLabels("severity", "critical")) + .targetLabels(createLabels("severity", "warning")) + .equalLabels(Arrays.asList("instance")) + .build(); + + alarmInhibitReduce.refreshInhibitRules(Collections.singletonList(rule)); + + // Create source alert with different instance + GroupAlert sourceAlert = createGroupAlert("firing", + createLabels("severity", "critical", "instance", "host1"), + Collections.emptyList()); + alarmInhibitReduce.inhibitAlarm(sourceAlert); + + // Create target alert with different instance + GroupAlert targetAlert = createGroupAlert("firing", + createLabels("severity", "warning", "instance", "host2"), + Collections.emptyList()); + alarmInhibitReduce.inhibitAlarm(targetAlert); + } + + @Test + void whenResolvedAlert_shouldNeverBeInhibited() { + AlertInhibit rule = AlertInhibit.builder() + .id(1L) + .enable(true) + .sourceLabels(createLabels("severity", "critical")) + .targetLabels(createLabels("severity", "warning")) + .build(); + + alarmInhibitReduce.refreshInhibitRules(Collections.singletonList(rule)); + + GroupAlert sourceAlert = createGroupAlert("firing", + createLabels("severity", "critical"), + Collections.emptyList()); + GroupAlert resolvedAlert = createGroupAlert("resolved", + createLabels("severity", "warning"), + Collections.emptyList()); + + alarmInhibitReduce.inhibitAlarm(sourceAlert); + alarmInhibitReduce.inhibitAlarm(resolvedAlert); + } + + @Test + void whenNullGroupAlert_shouldHandleGracefully() { + alarmInhibitReduce.inhibitAlarm(null); + verify(alarmSilenceReduce, never()).silenceAlarm(null); + } + + @Test + void whenEmptyAlertList_shouldPassThrough() { + GroupAlert alert = GroupAlert.builder() + .alerts(new ArrayList<>()) + .build(); + + alarmInhibitReduce.inhibitAlarm(alert); + verify(alarmSilenceReduce).silenceAlarm(alert); + } + + @Test + void whenMultipleSourceAlerts_shouldInhibitAllMatchingTargets() { + AlertInhibit rule = AlertInhibit.builder() + .id(1L) + .enable(true) + .sourceLabels(createLabels("severity", "critical")) + .targetLabels(createLabels("severity", "warning")) + .equalLabels(Collections.singletonList("instance")) + .build(); + + alarmInhibitReduce.refreshInhibitRules(Collections.singletonList(rule)); + + // Create source alerts + SingleAlert sourceAlert1 = createSingleAlert("firing", "fp1", + createLabels("severity", "critical", "instance", "host1")); + SingleAlert sourceAlert2 = createSingleAlert("firing", "fp2", + createLabels("severity", "critical", "instance", "host2")); + GroupAlert sourceGroupAlert = createGroupAlert("firing", null, + new ArrayList<>(Arrays.asList(sourceAlert1, sourceAlert2))); + alarmInhibitReduce.inhibitAlarm(sourceGroupAlert); + + // Create target alerts + SingleAlert targetAlert1 = createSingleAlert("firing", "fp3", + createLabels("severity", "warning", "instance", "host1")); + SingleAlert targetAlert2 = createSingleAlert("firing", "fp4", + createLabels("severity", "warning", "instance", "host2")); + SingleAlert targetAlert3 = createSingleAlert("firing", "fp5", + createLabels("severity", "warning", "instance", "host3")); + GroupAlert targetGroupAlert = createGroupAlert("firing", null, + new ArrayList<>(Arrays.asList(targetAlert1, targetAlert2, targetAlert3))); + + alarmInhibitReduce.inhibitAlarm(targetGroupAlert); + + assertEquals(1, targetGroupAlert.getAlerts().size()); + assertEquals("fp5", targetGroupAlert.getAlerts().get(0).getFingerprint()); + } + + @Test + void whenMultipleInhibitRules_shouldApplyAll() { + AlertInhibit rule1 = AlertInhibit.builder() + .id(1L) + .enable(true) + .sourceLabels(createLabels("severity", "critical")) + .targetLabels(createLabels("severity", "warning")) + .equalLabels(Arrays.asList("instance")) + .build(); + + AlertInhibit rule2 = AlertInhibit.builder() + .id(2L) + .enable(true) + .sourceLabels(createLabels("type", "disk")) + .targetLabels(createLabels("type", "memory")) + .equalLabels(Arrays.asList("host")) + .build(); + + alarmInhibitReduce.refreshInhibitRules(Arrays.asList(rule1, rule2)); + + // Test both rules being applied + SingleAlert sourceAlert = createSingleAlert("firing", "fp1", + createLabels("severity", "critical", "type", "disk", + "instance", "host1", "host", "server1")); + + GroupAlert sourceGroupAlert = GroupAlert.builder() + .alerts(Stream.of(sourceAlert).collect(Collectors.toList())) + .build(); + + alarmInhibitReduce.inhibitAlarm(sourceGroupAlert); + + // Create alerts that match different rules + SingleAlert targetAlert1 = createSingleAlert("firing", "fp2", + createLabels("severity", "warning", "instance", "host1")); + SingleAlert targetAlert2 = createSingleAlert("firing", "fp3", + createLabels("type", "memory", "host", "server1")); + + GroupAlert targetGroupAlert = GroupAlert.builder() + .alerts(new ArrayList<>(Arrays.asList(targetAlert1, targetAlert2))) + .status("firing") + .build(); + + alarmInhibitReduce.inhibitAlarm(targetGroupAlert); + + assertTrue(targetGroupAlert.getAlerts().isEmpty()); + } + + @Test + void whenSourceAlertExpires_shouldNotInhibit() throws InterruptedException { + // Configure short TTL for test + AlerterProperties.InhibitProperties inhibitProperties = new AlerterProperties.InhibitProperties(); + inhibitProperties.setTtl(100); + when(alerterProperties.getInhibit()).thenReturn(inhibitProperties); + alarmInhibitReduce.destroy(); + alarmInhibitReduce = new AlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties, + new VirtualThreadProperties(), false); + + AlertInhibit rule = AlertInhibit.builder() + .id(1L) + .enable(true) + .sourceLabels(createLabels("severity", "critical")) + .targetLabels(createLabels("severity", "warning")) + .equalLabels(Collections.singletonList("instance")) + .build(); + + alarmInhibitReduce.refreshInhibitRules(Collections.singletonList(rule)); + + // Process source alert + SingleAlert sourceAlert = createSingleAlert("firing", "fp1", + createLabels("severity", "critical", "instance", "host1")); + GroupAlert sourceGroupAlert = createGroupAlert("firing", + createLabels("severity", "critical", "instance", "host1"), + Stream.of(sourceAlert).collect(Collectors.toList())); + alarmInhibitReduce.inhibitAlarm(sourceGroupAlert); + + // Wait for source alert to expire + Thread.sleep(200); + + // Target alert should not be inhibited + SingleAlert targetAlert = createSingleAlert("firing", "fp2", + createLabels("severity", "warning", "instance", "host1")); + GroupAlert targetGroupAlert = createGroupAlert("firing", + createLabels("severity", "warning", "instance", "host1"), + Stream.of(targetAlert).collect(Collectors.toList())); + alarmInhibitReduce.inhibitAlarm(targetGroupAlert); + + verify(alarmSilenceReduce).silenceAlarm(targetGroupAlert); + } + + @Test + void dispatchCleanupCacheRunsOnVirtualThread() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + alarmInhibitReduce.destroy(); + alarmInhibitReduce = new TestAlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties, + new VirtualThreadProperties(), latch, virtualThread, null, null, null, null, null); + + alarmInhibitReduce.dispatchCleanupCache(); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void dispatchCleanupCacheDoesNotRunConcurrently() throws Exception { + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + AtomicInteger maxConcurrent = new AtomicInteger(); + alarmInhibitReduce.destroy(); + alarmInhibitReduce = new TestAlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties, + new VirtualThreadProperties(), null, null, firstStarted, releaseFirst, secondStarted, + maxConcurrent, new AtomicInteger()); + + alarmInhibitReduce.dispatchCleanupCache(); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + alarmInhibitReduce.dispatchCleanupCache(); + assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS)); + + releaseFirst.countDown(); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + assertEquals(1, maxConcurrent.get()); + } + + private GroupAlert createGroupAlert(String status, Map<String, String> labels, List<SingleAlert> alerts) { + return GroupAlert.builder() + .status(status) + .commonLabels(labels) + .alerts(alerts) + .build(); + } + + private Map<String, String> createLabels(String... keyValues) { + Map<String, String> labels = new HashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + labels.put(keyValues[i], keyValues[i + 1]); + } + return labels; + } + + private SingleAlert createSingleAlert(String status, String fingerprint, Map<String, String> labels) { + return SingleAlert.builder() + .status(status) + .fingerprint(fingerprint) + .labels(labels) + .build(); + } + + private static final class TestAlarmInhibitReduce extends AlarmInhibitReduce { + + private final CountDownLatch virtualThreadLatch; + + private final AtomicBoolean virtualThread; + + private final CountDownLatch firstStarted; + + private final CountDownLatch releaseFirst; + + private final CountDownLatch secondStarted; + + private final AtomicInteger maxConcurrent; + + private final AtomicInteger concurrent; + + private final AtomicInteger invocations; + + private TestAlarmInhibitReduce(AlarmSilenceReduce alarmSilenceReduce, AlertInhibitDao alertInhibitDao, + AlerterProperties alerterProperties, VirtualThreadProperties properties, + CountDownLatch virtualThreadLatch, AtomicBoolean virtualThread, + CountDownLatch firstStarted, CountDownLatch releaseFirst, + CountDownLatch secondStarted, AtomicInteger maxConcurrent, + AtomicInteger invocations) { + super(alarmSilenceReduce, alertInhibitDao, alerterProperties, properties, false); + this.virtualThreadLatch = virtualThreadLatch; + this.virtualThread = virtualThread; + this.firstStarted = firstStarted; + this.releaseFirst = releaseFirst; + this.secondStarted = secondStarted; + this.maxConcurrent = maxConcurrent; + this.invocations = invocations; + this.concurrent = maxConcurrent == null ? null : new AtomicInteger(); + } + + @Override + void beforeCleanupCacheRun() { + if (virtualThread != null) { + virtualThread.set(Thread.currentThread().isVirtual()); + } + if (virtualThreadLatch != null) { + virtualThreadLatch.countDown(); + } + if (maxConcurrent == null || invocations == null) { + return; + } + int running = concurrent.incrementAndGet(); + maxConcurrent.accumulateAndGet(running, Math::max); + int currentInvocation = invocations.incrementAndGet(); + try { + if (currentInvocation == 1 && firstStarted != null && releaseFirst != null) { + firstStarted.countDown(); + releaseFirst.await(5, TimeUnit.SECONDS); + } else if (currentInvocation == 2 && secondStarted != null) { + secondStarted.countDown(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + concurrent.decrementAndGet(); + } + } + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduceTest.java new file mode 100644 index 0000000..2c9d7d0 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduceTest.java @@ -0,0 +1,241 @@ +/* + * 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.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.hertzbeat.alert.dao.AlertSilenceDao; +import org.apache.hertzbeat.alert.notice.AlertNoticeDispatch; +import org.apache.hertzbeat.common.cache.CacheFactory; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test for {@link AlarmSilenceReduce} + */ +class AlarmSilenceReduceTest { + + @Mock + private AlertSilenceDao alertSilenceDao; + + @Mock + private AlertNoticeDispatch alertNoticeDispatch; + + private AlarmSilenceReduce alarmSilenceReduce; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + CacheFactory.clearAlertSilenceCache(); + alarmSilenceReduce = new AlarmSilenceReduce(alertSilenceDao, alertNoticeDispatch); + } + + @Test + void whenNoSilenceRules_shouldForwardAlert() { + when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.emptyList()); + + GroupAlert alert = createGroupAlert("firing", createLabels("service", "web")); + alarmSilenceReduce.silenceAlarm(alert); + + verify(alertNoticeDispatch).dispatchAlarm(alert); + } + + @Test + void whenMatchingSilenceRule_shouldNotForwardAlert() { + + // Create silence rule + AlertSilence silenceRule = AlertSilence.builder() + .enable(true) + .matchAll(false) + .type((byte) 0) // once + .labels(createLabels("service", "web")) + .periodStart(LocalDateTime.now().minusHours(1).atZone(ZoneId.systemDefault())) + .periodEnd(LocalDateTime.now().plusHours(1).atZone(ZoneId.systemDefault())) + .times(0) + .build(); + + when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule)); + when(alertSilenceDao.save(any(AlertSilence.class))).thenReturn(silenceRule); + + GroupAlert alert = createGroupAlert("firing", createLabels("service", "web")); + + alarmSilenceReduce.silenceAlarm(alert); + + verify(alertNoticeDispatch, never()).dispatchAlarm(alert); + verify(alertSilenceDao).save(silenceRule); + } + + @Test + void whenMatchingCyclicSilenceRuleShouldNotForwardAlert() { + LocalDateTime now = LocalDateTime.now(); + // Create cyclic silence rule + AlertSilence silenceRule = AlertSilence.builder() + .enable(true) + .matchAll(false) + .type((byte) 1) // cyclic + .labels(createLabels("service", "web")) + .periodStart(now.minusMinutes(30).atZone(ZoneId.systemDefault())) + .periodEnd(now.plusMinutes(30).atZone(ZoneId.systemDefault())) + .days(Collections.singletonList((byte) now.getDayOfWeek().getValue())) + .times(0) + .build(); + + when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule)); + when(alertSilenceDao.save(any(AlertSilence.class))).thenReturn(silenceRule); + + GroupAlert alert = createGroupAlert("firing", createLabels("service", "web")); + + alarmSilenceReduce.silenceAlarm(alert); + + verify(alertNoticeDispatch, never()).dispatchAlarm(alert); + verify(alertSilenceDao).save(silenceRule); + } + + @Test + void whenCyclicSilenceRuleTimeMatchesButDateDifferent_shouldNotForwardAlert() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime yesterday = now.minusDays(1); + LocalDateTime start = yesterday.withHour(now.getHour()) + .withMinute(Math.max(0, now.getMinute() - 10)) + .withSecond(0) + .withNano(0); + LocalDateTime end = yesterday.withHour(now.getHour()) + .withMinute(Math.min(59, now.getMinute() + 10)) + .withSecond(59) + .withNano(0); + + AlertSilence silenceRule = AlertSilence.builder() + .enable(true) + .matchAll(false) + .type((byte) 1) + .labels(createLabels("service", "web")) + .periodStart(start.atZone(ZoneId.systemDefault())) + .periodEnd(end.atZone(ZoneId.systemDefault())) + .days(Collections.singletonList((byte) now.getDayOfWeek().getValue())) + .times(0) + .build(); + + when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule)); + when(alertSilenceDao.save(any(AlertSilence.class))).thenReturn(silenceRule); + + GroupAlert alert = createGroupAlert("firing", createLabels("service", "web")); + alarmSilenceReduce.silenceAlarm(alert); + + verify(alertNoticeDispatch, never()).dispatchAlarm(alert); + verify(alertSilenceDao).save(silenceRule); + } + + @Test + void whenMatchingCrossMidnightCyclicSilenceRule_shouldNotForwardAlert() { + LocalDateTime now = LocalDateTime.now(); + AlertSilence silenceRule = AlertSilence.builder() + .enable(true) + .matchAll(false) + .type((byte) 1) + .labels(createLabels("service", "web")) + .periodStart(now.minusHours(1).atZone(ZoneId.systemDefault())) + .periodEnd(now.minusHours(2).atZone(ZoneId.systemDefault())) + .days(Collections.singletonList((byte) now.getDayOfWeek().getValue())) + .times(0) + .build(); + + when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule)); + when(alertSilenceDao.save(any(AlertSilence.class))).thenReturn(silenceRule); + + GroupAlert alert = createGroupAlert("firing", createLabels("service", "web")); + alarmSilenceReduce.silenceAlarm(alert); + + verify(alertNoticeDispatch, never()).dispatchAlarm(alert); + verify(alertSilenceDao).save(silenceRule); + } + + @Test + void whenCrossMidnightCyclicSilenceRuleDoesNotIncludeCurrentDay_shouldForwardAlert() { + LocalDateTime now = LocalDateTime.now(); + byte previousDay = (byte) now.minusDays(1).getDayOfWeek().getValue(); + AlertSilence silenceRule = AlertSilence.builder() + .enable(true) + .matchAll(false) + .type((byte) 1) + .labels(createLabels("service", "web")) + .periodStart(now.minusHours(1).atZone(ZoneId.systemDefault())) + .periodEnd(now.minusHours(2).atZone(ZoneId.systemDefault())) + .days(Collections.singletonList(previousDay)) + .times(0) + .build(); + + when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule)); + + GroupAlert alert = createGroupAlert("firing", createLabels("service", "web")); + alarmSilenceReduce.silenceAlarm(alert); + + verify(alertNoticeDispatch).dispatchAlarm(alert); + verify(alertSilenceDao, never()).save(any()); + } + + @Test + void whenSilenceRuleExpired_shouldForwardAlert() { + AlertSilence silenceRule = AlertSilence.builder() + .enable(true) + .matchAll(false) + .type((byte) 0) + .labels(createLabels("service", "web")) + .periodStart(LocalDateTime.now().minusHours(2).atZone(ZoneId.systemDefault())) + .periodEnd(LocalDateTime.now().minusHours(1).atZone(ZoneId.systemDefault())) + .times(0) + .build(); + + when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule)); + + GroupAlert alert = createGroupAlert("firing", createLabels("service", "web")); + + alarmSilenceReduce.silenceAlarm(alert); + + verify(alertNoticeDispatch).dispatchAlarm(alert); + verify(alertSilenceDao, never()).save(any()); + } + + private GroupAlert createGroupAlert(String status, Map<String, String> labels) { + return GroupAlert.builder() + .status(status) + .groupLabels(labels) + .commonLabels(labels) + .build(); + } + + private Map<String, String> createLabels(String... keyValues) { + Map<String, String> labels = new HashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + labels.put(keyValues[i], keyValues[i + 1]); + } + return labels; + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineExcelImExportServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineExcelImExportServiceTest.java new file mode 100644 index 0000000..885898f --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineExcelImExportServiceTest.java @@ -0,0 +1,155 @@ +/* + * 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.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.hertzbeat.alert.dto.AlertDefineDTO; +import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO; +import org.apache.hertzbeat.alert.service.impl.AlertDefineExcelImExportServiceImpl; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.common.util.export.ExcelExportUtils; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * test case for {@link AlertDefineExcelImExportServiceImpl} + */ +@ExtendWith(MockitoExtension.class) +public class AlertDefineExcelImExportServiceTest { + + @InjectMocks + private AlertDefineExcelImExportServiceImpl alertDefineExcelImExportService; + + private Workbook workbook; + private Sheet sheet; + + @BeforeEach + public void setUp() throws IOException { + + Workbook initialWorkbook = WorkbookFactory.create(true); + Sheet initialSheet = ExcelExportUtils.setSheet("Test sheet", initialWorkbook, AlertDefineDTO.class); + + Row row = initialSheet.createRow(1); + row.createCell(0).setCellValue("app1"); + row.createCell(1).setCellValue("metric1"); + row.createCell(2).setCellValue("expr1"); + row.createCell(3).setCellValue(10); + row.createCell(4).setCellValue(1); + row.createCell(5).setCellValue(JsonUtil.toJson(Map.of("key", "value"))); + row.createCell(6).setCellValue(JsonUtil.toJson(Map.of("key", "value"))); + row.createCell(7).setCellValue("template1"); + row.createCell(8).setCellValue(true); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(toByteArray(initialWorkbook)); + + workbook = WorkbookFactory.create(inputStream); + sheet = workbook.getSheetAt(0); + } + + @Test + public void testParseImport() throws IOException { + + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(toByteArray(workbook))) { + List<ExportAlertDefineDTO> result = alertDefineExcelImExportService.parseImport(inputStream); + + assertEquals(1, result.size()); + AlertDefineDTO alertDefineDTO = result.get(0).getAlertDefine(); + assertEquals("app1", alertDefineDTO.getName()); + assertEquals("metric1", alertDefineDTO.getType()); + assertEquals("expr1", alertDefineDTO.getExpr()); + assertEquals(10, alertDefineDTO.getPeriod()); + assertEquals(1, alertDefineDTO.getTimes()); + assertEquals(Map.of("key", "value"), alertDefineDTO.getLabels()); + assertEquals(Map.of("key", "value"), alertDefineDTO.getAnnotations()); + assertEquals("template1", alertDefineDTO.getTemplate()); + assertTrue(alertDefineDTO.getEnable()); + } + } + + @Test + public void testWriteOs() throws IOException { + + List<ExportAlertDefineDTO> exportAlertDefineList = new ArrayList<>(); + ExportAlertDefineDTO exportAlertDefineDTO = new ExportAlertDefineDTO(); + AlertDefineDTO alertDefineDTO = new AlertDefineDTO(); + alertDefineDTO.setName("app1"); + alertDefineDTO.setType("metric1"); + alertDefineDTO.setExpr("expr1"); + alertDefineDTO.setPeriod(10); + alertDefineDTO.setTimes(1); + alertDefineDTO.setLabels(Map.of("key", "value")); + alertDefineDTO.setAnnotations(Map.of("key", "value")); + alertDefineDTO.setTemplate("template1"); + alertDefineDTO.setEnable(true); + exportAlertDefineDTO.setAlertDefine(alertDefineDTO); + exportAlertDefineList.add(exportAlertDefineDTO); + + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + alertDefineExcelImExportService.writeOs(exportAlertDefineList, outputStream); + + try (Workbook resultWorkbook = WorkbookFactory.create(new ByteArrayInputStream(outputStream.toByteArray()))) { + Sheet resultSheet = resultWorkbook.getSheetAt(0); + Row headerRow = resultSheet.getRow(0); + assertEquals("Name", headerRow.getCell(0).getStringCellValue()); + assertEquals("Type", headerRow.getCell(1).getStringCellValue()); + assertEquals("Expr", headerRow.getCell(2).getStringCellValue()); + assertEquals("Period", headerRow.getCell(3).getStringCellValue()); + assertEquals("Times", headerRow.getCell(4).getStringCellValue()); + assertEquals("Labels", headerRow.getCell(5).getStringCellValue()); + assertEquals("Annotations", headerRow.getCell(6).getStringCellValue()); + assertEquals("Template", headerRow.getCell(7).getStringCellValue()); + assertEquals("Enable", headerRow.getCell(8).getStringCellValue()); + + Row dataRow = resultSheet.getRow(1); + assertEquals("app1", dataRow.getCell(0).getStringCellValue()); + assertEquals("metric1", dataRow.getCell(1).getStringCellValue()); + assertEquals("expr1", dataRow.getCell(2).getStringCellValue()); + assertEquals(10, (int) dataRow.getCell(3).getNumericCellValue()); + assertEquals(1, (int) dataRow.getCell(4).getNumericCellValue()); + assertEquals(JsonUtil.toJson(Map.of("key", "value")), dataRow.getCell(5).getStringCellValue()); + assertEquals(JsonUtil.toJson(Map.of("key", "value")), dataRow.getCell(6).getStringCellValue()); + assertEquals("template1", dataRow.getCell(7).getStringCellValue()); + assertTrue(dataRow.getCell(8).getBooleanCellValue()); + } + } + } + + private byte[] toByteArray(Workbook workbook) throws IOException { + + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + workbook.write(bos); + return bos.toByteArray(); + } + } + +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineJsonImExportServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineJsonImExportServiceTest.java new file mode 100644 index 0000000..d0727c0 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineJsonImExportServiceTest.java @@ -0,0 +1,112 @@ +/* + * 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.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.apache.hertzbeat.alert.dto.AlertDefineDTO; +import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO; +import org.apache.hertzbeat.alert.service.impl.AlertDefineJsonImExportServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * test case for {@link AlertDefineJsonImExportServiceImpl} + */ + +class AlertDefineJsonImExportServiceTest { + + private AlertDefineJsonImExportServiceImpl service; + + @SuppressWarnings("checkstyle:OperatorWrap") + private static final String JSON_DATA = "[{\"alertDefine\":{\"name\":\"App1\",\"type\":\"realtime\"," + + "\"expr\":\"Expr1\",\"period\":3000,\"times\":3," + + "\"enable\":true,\"template\":\"Template1\"}}]"; + + private InputStream inputStream; + private List<ExportAlertDefineDTO> alertDefineList; + + @BeforeEach + public void setup() { + service = new AlertDefineJsonImExportServiceImpl(); + + inputStream = new ByteArrayInputStream(JSON_DATA.getBytes(StandardCharsets.UTF_8)); + + AlertDefineDTO alertDefine = new AlertDefineDTO(); + alertDefine.setName("App1"); + alertDefine.setType("realtime"); + alertDefine.setExpr("Expr1"); + alertDefine.setPeriod(3000); + alertDefine.setTimes(3); + alertDefine.setEnable(true); + alertDefine.setTemplate("Template1"); + + ExportAlertDefineDTO exportAlertDefine = new ExportAlertDefineDTO(); + exportAlertDefine.setAlertDefine(alertDefine); + + alertDefineList = List.of(exportAlertDefine); + } + + @Test + void testParseImport() { + List<ExportAlertDefineDTO> result = service.parseImport(inputStream); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("App1", result.get(0).getAlertDefine().getName()); + assertEquals("realtime", result.get(0).getAlertDefine().getType()); + } + + @Test + void testParseImportFailed() { + InputStream invalidInputStream = new ByteArrayInputStream("invalid json".getBytes(StandardCharsets.UTF_8)); + + List<ExportAlertDefineDTO> result = service.parseImport(invalidInputStream); + + assertNull(result); + } + + @Test + void testWriteOs() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + service.writeOs(alertDefineList, outputStream); + + String result = outputStream.toString(StandardCharsets.UTF_8); + assertNotNull(result); + assertTrue(result.contains("App1")); + assertTrue(result.contains("realtime")); + } + + @Test + void testType() { + assertEquals("JSON", service.type()); + } + + @Test + void testGetFileName() { + assertTrue(service.getFileName().endsWith(".json")); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java new file mode 100644 index 0000000..de29d2c --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java @@ -0,0 +1,349 @@ +/* + * 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.service; + +import com.google.common.collect.Lists; +import org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler; +import org.apache.hertzbeat.alert.dao.AlertDefineDao; +import org.apache.hertzbeat.alert.service.impl.AlertDefineServiceImpl; +import org.apache.hertzbeat.base.dao.LabelDao; +import org.apache.hertzbeat.base.service.LabelService; +import org.apache.hertzbeat.common.cache.CacheFactory; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC; +import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME; +import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC; +import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anySet; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test case for {@link AlertDefineService} + */ +@ExtendWith(MockitoExtension.class) +class AlertDefineServiceTest { + + private AlertDefine alertDefine; + + @Mock + private AlertDefineDao alertDefineDao; + + @Mock + private LabelService labelService; + + @Mock + private LabelDao labelDao; + + @Mock + private PeriodicAlertRuleScheduler periodicAlertRuleScheduler; + + @Mock + private List<AlertDefineImExportService> alertDefineImExportServiceList; + + @Mock + private DataSourceService dataSourceService; + + @InjectMocks + private AlertDefineServiceImpl alertDefineService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(this.alertDefineService, "alertDefineDao", alertDefineDao); + ReflectionTestUtils.setField(this.alertDefineService, "periodicAlertRuleScheduler", periodicAlertRuleScheduler); + ReflectionTestUtils.setField(this.alertDefineService, "labelService", labelService); + ReflectionTestUtils.setField(this.alertDefineService, "labelDao", labelDao); + + this.alertDefine = AlertDefine.builder() + .id(1L) + .expr("1 > 0") + .times(1) + .template("template") + .creator("tom") + .modifier("tom") + .build(); + } + + @Test + void validate() { + assertDoesNotThrow(() -> alertDefineService.validate(alertDefine, true)); + assertDoesNotThrow(() -> alertDefineService.validate(alertDefine, false)); + } + + @Test + void addAlertDefine() { + assertDoesNotThrow(() -> this.alertDefineService.addAlertDefine(alertDefine)); + when(alertDefineDao.saveAndFlush(alertDefine)).thenThrow(new RuntimeException()); + assertThrows(RuntimeException.class, () -> this.alertDefineService.addAlertDefine(alertDefine)); + } + + @Test + void modifyAlertDefine() { + AlertDefine alertDefine = AlertDefine.builder().id(1L).build(); + when(alertDefineDao.saveAndFlush(alertDefine)).thenReturn(alertDefine); + assertDoesNotThrow(() -> this.alertDefineService.modifyAlertDefine(alertDefine)); + reset(); + when(alertDefineDao.saveAndFlush(alertDefine)).thenThrow(new RuntimeException()); + assertThrows(RuntimeException.class, () -> this.alertDefineService.modifyAlertDefine(alertDefine)); + } + + @Test + void deleteAlertDefine() { + long id = 1L; + doNothing().doThrow(new RuntimeException()).when(alertDefineDao).deleteById(id); + assertDoesNotThrow(() -> alertDefineService.deleteAlertDefine(id)); + assertThrows(RuntimeException.class, () -> alertDefineService.deleteAlertDefine(id)); + } + + @Test + void getAlertDefine() { + long id = 1L; + AlertDefine alertDefine = AlertDefine.builder().id(id).build(); + when(alertDefineDao.findById(id)).thenReturn(Optional.of(alertDefine)); + assertDoesNotThrow(() -> alertDefineService.getAlertDefine(id)); + } + + @Test + void deleteAlertDefines() { + doNothing().when(alertDefineDao).deleteAlertDefinesByIdIn(anySet()); + assertDoesNotThrow(() -> alertDefineService.deleteAlertDefines(new HashSet<>(1))); + } + + @Test + void getAlertDefines() { + when(alertDefineDao.findAll(any(Specification.class), any(PageRequest.class))).thenReturn(Page.empty()); + assertNotNull(alertDefineService.getAlertDefines(null, null, "id", "desc", 1, 10)); + verify(alertDefineDao, times(1)).findAll(any(Specification.class), any(PageRequest.class)); + } + + @Test + void getDefinePreview() { + String expr = "http_server_requests_seconds_count > 10"; + + Map<String, Object> countValue1 = new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 1307); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/actuator/prometheus"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }; + when(dataSourceService.calculate(eq("promql"), eq(expr))).thenReturn(Lists.newArrayList(countValue1)); + List<Map<String, Object>> result = alertDefineService.getDefinePreview("promql", METRIC_ALERT_THRESHOLD_TYPE_PERIODIC, expr); + assertNotNull(result); + assertEquals(1307, result.get(0).get("__value__")); + + result = alertDefineService.getDefinePreview("promql", METRIC_ALERT_THRESHOLD_TYPE_PERIODIC, null); + assertEquals(0, result.size()); + + result = alertDefineService.getDefinePreview("promql", METRIC_ALERT_THRESHOLD_TYPE_REALTIME, null); + assertEquals(0, result.size()); + } + + @Test + void getMetricsRealTimeAlertDefines() { + // Test cache hit scenario + List<AlertDefine> cachedAlertDefines = Lists.newArrayList( + AlertDefine.builder().id(1L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build(), + AlertDefine.builder().id(2L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build() + ); + + try (MockedStatic<CacheFactory> cacheFactoryMock = Mockito.mockStatic(CacheFactory.class)) { + // Mock cache hit + cacheFactoryMock.when(CacheFactory::getMetricsAlertDefineCache).thenReturn(cachedAlertDefines); + + List<AlertDefine> result = alertDefineService.getMetricsRealTimeAlertDefines(); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(1L, result.get(0).getId()); + assertEquals(2L, result.get(1).getId()); + + // Verify no database query was called + verify(alertDefineDao, times(0)).findAlertDefinesByTypeAndEnableTrue(any()); + cacheFactoryMock.verify(() -> CacheFactory.setMetricsAlertDefineCache(any()), times(0)); + } + + // Test cache miss scenario + List<AlertDefine> dbAlertDefines = Lists.newArrayList( + AlertDefine.builder().id(3L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build() + ); + + try (MockedStatic<CacheFactory> cacheFactoryMock = Mockito.mockStatic(CacheFactory.class)) { + // Mock cache miss + cacheFactoryMock.when(CacheFactory::getMetricsAlertDefineCache).thenReturn(null); + when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME)) + .thenReturn(dbAlertDefines); + + List<AlertDefine> result = alertDefineService.getMetricsRealTimeAlertDefines(); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(3L, result.get(0).getId()); + + // Verify database query and cache setting were called + verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME); + cacheFactoryMock.verify(() -> CacheFactory.setMetricsAlertDefineCache(dbAlertDefines), times(1)); + } + } + + @Test + void getLogRealTimeAlertDefines() { + // Test cache hit scenario + List<AlertDefine> cachedAlertDefines = Lists.newArrayList( + AlertDefine.builder().id(4L).type(LOG_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build(), + AlertDefine.builder().id(5L).type(LOG_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build() + ); + + try (MockedStatic<CacheFactory> cacheFactoryMock = Mockito.mockStatic(CacheFactory.class)) { + // Mock cache hit + cacheFactoryMock.when(CacheFactory::getLogAlertDefineCache).thenReturn(cachedAlertDefines); + + List<AlertDefine> result = alertDefineService.getLogRealTimeAlertDefines(); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(4L, result.get(0).getId()); + assertEquals(5L, result.get(1).getId()); + + // Verify no database query was called + verify(alertDefineDao, times(0)).findAlertDefinesByTypeAndEnableTrue(any()); + cacheFactoryMock.verify(() -> CacheFactory.setLogAlertDefineCache(any()), times(0)); + } + + // Test cache miss scenario + List<AlertDefine> dbAlertDefines = Lists.newArrayList( + AlertDefine.builder().id(6L).type(LOG_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build() + ); + + try (MockedStatic<CacheFactory> cacheFactoryMock = Mockito.mockStatic(CacheFactory.class)) { + // Mock cache miss + cacheFactoryMock.when(CacheFactory::getLogAlertDefineCache).thenReturn(null); + when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME)) + .thenReturn(dbAlertDefines); + + List<AlertDefine> result = alertDefineService.getLogRealTimeAlertDefines(); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(6L, result.get(0).getId()); + + // Verify database query and cache setting were called + verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME); + cacheFactoryMock.verify(() -> CacheFactory.setLogAlertDefineCache(dbAlertDefines), times(1)); + } + } + + @Test + void getAlertDefinesByType() { + List<AlertDefine> mockAlertDefines = Lists.newArrayList( + AlertDefine.builder().id(7L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build(), + AlertDefine.builder().id(8L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build() + ); + + // Test valid metric realtime alert type + when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME)) + .thenReturn(mockAlertDefines); + + List<AlertDefine> result = alertDefineService.getAlertDefinesByType(METRIC_ALERT_THRESHOLD_TYPE_REALTIME); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(7L, result.get(0).getId()); + assertEquals(8L, result.get(1).getId()); + verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME); + + // Test valid metric periodic alert type + reset(alertDefineDao); + when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) + .thenReturn(Lists.newArrayList()); + + result = alertDefineService.getAlertDefinesByType(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC); + + assertNotNull(result); + assertEquals(0, result.size()); + verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC); + + // Test valid log realtime alert type + reset(alertDefineDao); + when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME)) + .thenReturn(mockAlertDefines); + + result = alertDefineService.getAlertDefinesByType(LOG_ALERT_THRESHOLD_TYPE_REALTIME); + + assertNotNull(result); + assertEquals(2, result.size()); + verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME); + + // Test valid log periodic alert type + reset(alertDefineDao); + when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) + .thenReturn(Lists.newArrayList()); + + result = alertDefineService.getAlertDefinesByType(LOG_ALERT_THRESHOLD_TYPE_PERIODIC); + + assertNotNull(result); + assertEquals(0, result.size()); + verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC); + + // Test empty string type + assertThrows(IllegalArgumentException.class, () -> alertDefineService.getAlertDefinesByType("")); + + // Test null type + assertThrows(IllegalArgumentException.class, () -> alertDefineService.getAlertDefinesByType(null)); + + // Test invalid type + assertThrows(IllegalArgumentException.class, () -> alertDefineService.getAlertDefinesByType("invalid_type")); + } + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineYamlImExportServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineYamlImExportServiceTest.java new file mode 100644 index 0000000..692b633 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineYamlImExportServiceTest.java @@ -0,0 +1,156 @@ +/* + * 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.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.apache.hertzbeat.alert.dto.AlertDefineDTO; +import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO; +import org.apache.hertzbeat.alert.service.impl.AlertDefineYamlImExportServiceImpl; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.yaml.snakeyaml.Yaml; + +/** + * test case for {@link AlertDefineYamlImExportServiceImpl} +*/ +@ExtendWith(MockitoExtension.class) +class AlertDefineYamlImExportServiceTest { + + @InjectMocks + private AlertDefineYamlImExportServiceImpl service; + + private static final String YAML_DATA = + """ + - alertDefine: + name: App1 + type: realtime + expr: Expr1 + period: 3000 + times: 3 + enable: true + template: Template1 + """; + + private InputStream inputStream; + private List<ExportAlertDefineDTO> alertDefineList; + + @BeforeEach + public void setup() { + + inputStream = new ByteArrayInputStream(YAML_DATA.getBytes(StandardCharsets.UTF_8)); + + AlertDefineDTO alertDefine = new AlertDefineDTO(); + alertDefine.setName("App1"); + alertDefine.setType("realtime"); + alertDefine.setPeriod(3000); + alertDefine.setTimes(3); + alertDefine.setExpr("Expr1"); + alertDefine.setEnable(true); + alertDefine.setTemplate("Template1"); + + ExportAlertDefineDTO exportAlertDefine = new ExportAlertDefineDTO(); + exportAlertDefine.setAlertDefine(alertDefine); + + alertDefineList = List.of(exportAlertDefine); + } + + @Test + void testParseImport() { + + List<ExportAlertDefineDTO> result = service.parseImport(inputStream); + + assertNotNull(result); + assertEquals(1, result.size()); + + InputStream inputStream = new ByteArrayInputStream(JsonUtil.toJson(alertDefineList) + .getBytes(StandardCharsets.UTF_8)); + Yaml yaml = new Yaml(); + + assertEquals(yaml.load(inputStream), result); + } + + @Test + void testParseImportFailed() { + + InputStream faultyInputStream = mock(InputStream.class); + try { + when(faultyInputStream.read( + any(byte[].class), + anyInt(), anyInt()) + ).thenThrow(new IOException("Test Exception")); + + RuntimeException exception = assertThrows( + RuntimeException.class, + () -> service.parseImport(faultyInputStream) + ); + assertEquals("java.io.IOException: Test Exception", exception.getMessage()); + } catch (IOException e) { + + fail("Mocking IOException failed"); + } + } + + @Test + void testWriteOs() { + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + service.writeOs(alertDefineList, outputStream); + String yamlOutput = outputStream.toString(StandardCharsets.UTF_8); + + assertTrue(yamlOutput.contains("name: App1")); + assertTrue(yamlOutput.contains("type: realtime")); + assertTrue(yamlOutput.contains("expr: Expr1")); + } + + @Test + void testWriteOsFailed() { + + OutputStream faultyOutputStream = mock(OutputStream.class); + + try { + doThrow(new IOException("Test Exception")).when(faultyOutputStream).write(any(byte[].class), anyInt(), anyInt()); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> service.writeOs(alertDefineList, faultyOutputStream)); + assertEquals("java.io.IOException: Test Exception", exception.getMessage()); + } catch (IOException e) { + + fail("Mocking IOException failed"); + } + } + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertGroupConvergeServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertGroupConvergeServiceTest.java new file mode 100644 index 0000000..f65d307 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertGroupConvergeServiceTest.java @@ -0,0 +1,124 @@ +/* + * 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.service; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; + +import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao; +import org.apache.hertzbeat.alert.reduce.AlarmGroupReduce; +import org.apache.hertzbeat.alert.service.impl.AlertGroupConvergeServiceImpl; +import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anySet; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; + +/** + * test case for {@link AlertGroupConvergeServiceImpl} +*/ + +@ExtendWith(MockitoExtension.class) +class AlertGroupConvergeServiceTest { + + @Mock + private AlertGroupConvergeDao alertGroupConvergeDao; + + @InjectMocks + private AlertGroupConvergeServiceImpl alertGroupConvergeService; + + @Mock + private AlarmGroupReduce alarmGroupReduce; + + @Test + public void testAddAlertGroupConverge() { + AlertGroupConverge alertGroupConverge = new AlertGroupConverge(); + doNothing().when(alarmGroupReduce).refreshGroupDefines(anyList()); + when(alertGroupConvergeDao.save(alertGroupConverge)).thenReturn(alertGroupConverge); + alertGroupConvergeService.addAlertGroupConverge(alertGroupConverge); + verify(alertGroupConvergeDao, times(1)).save(alertGroupConverge); + verify(alarmGroupReduce, times(1)).refreshGroupDefines(anyList()); + } + + + @Test + public void testModifyAlertGroupConverge() { + AlertGroupConverge alertGroupConverge = new AlertGroupConverge(); + doNothing().when(alarmGroupReduce).refreshGroupDefines(anyList()); + when(alertGroupConvergeDao.save(alertGroupConverge)).thenReturn(alertGroupConverge); + alertGroupConvergeService.modifyAlertGroupConverge(alertGroupConverge); + verify(alertGroupConvergeDao, times(1)).save(alertGroupConverge); + verify(alarmGroupReduce, times(1)).refreshGroupDefines(anyList()); + } + + @Test + public void testGetAlertGroupConverge() { + + long convergeId = 1L; + AlertGroupConverge alertGroupConverge = new AlertGroupConverge(); + when(alertGroupConvergeDao.findById(convergeId)).thenReturn(Optional.of(alertGroupConverge)); + AlertGroupConverge result = alertGroupConvergeService.getAlertGroupConverge(convergeId); + verify(alertGroupConvergeDao, times(1)).findById(convergeId); + assertEquals(alertGroupConverge, result); + } + + @Test + public void testDeleteAlertGroupConverges() { + + doNothing().when(alertGroupConvergeDao).deleteAlertGroupConvergesByIdIn(anySet()); + doNothing().when(alarmGroupReduce).refreshGroupDefines(anyList()); + alertGroupConvergeService.deleteAlertGroupConverges(new HashSet<>(Arrays.asList(1L, 2L))); + verify(alertGroupConvergeDao, times(1)).deleteAlertGroupConvergesByIdIn(anySet()); + verify(alarmGroupReduce, times(1)).refreshGroupDefines(anyList()); + } + + @Test + public void testGetAlertGroupConverges() { + + Page<AlertGroupConverge> page = new PageImpl<>(Collections.emptyList()); + when(alertGroupConvergeDao.findAll( + any(Specification.class), + any(Pageable.class)) + ).thenReturn(page); + + Page<AlertGroupConverge> result = alertGroupConvergeService.getAlertGroupConverges(null, null, "id", "desc", 1, 10); + + verify(alertGroupConvergeDao, times(1)).findAll( + any(Specification.class), + any(PageRequest.class) + ); + assertEquals(page, result); + } + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertServiceTest.java new file mode 100644 index 0000000..e3f3cfd --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertServiceTest.java @@ -0,0 +1,102 @@ +/* + * 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.service; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.dao.GroupAlertDao; +import org.apache.hertzbeat.alert.dao.SingleAlertDao; +import org.apache.hertzbeat.alert.dto.AlertSummary; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.impl.AlertServiceImpl; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link AlertService} +*/ +@ExtendWith(MockitoExtension.class) +class AlertServiceTest { + @Mock + private GroupAlertDao groupAlertDao; + + @Mock + private SingleAlertDao singleAlertDao; + + @Mock + private AlarmCommonReduce alarmCommonReduce; + + @InjectMocks + private AlertServiceImpl alertService; + + @BeforeEach + void setUp() { + } + + @Test + void deleteGroupAlerts() { + HashSet<Long> ids = new HashSet<>(); + ids.add(1L); + ids.add(2L); + assertDoesNotThrow(() -> alertService.deleteGroupAlerts(ids)); + verify(groupAlertDao, times(1)).deleteGroupAlertsByIdIn(ids); + } + + + @Test + void editGroupAlertStatus() { + String status = "firing"; + List<Long> ids = List.of(1L, 2L, 3L); + assertDoesNotThrow(() -> alertService.editGroupAlertStatus(status, ids)); + verify(groupAlertDao, times(1)).updateGroupAlertsStatus(status, ids); + } + + @Test + void testGetAlertsSummary() { + SingleAlert alert = new SingleAlert(); + alert.setLabels(Collections.singletonMap(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL)); + + when(singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING)).thenReturn(Collections.singletonList(alert)); + when(singleAlertDao.count()).thenReturn(10L); + + AlertSummary summary = alertService.getAlertsSummary(); + + assertNotNull(summary); + assertEquals(1, summary.getPriorityCriticalNum()); + assertEquals(0, summary.getPriorityEmergencyNum()); + assertEquals(0, summary.getPriorityWarningNum()); + assertEquals(10L, summary.getTotal()); + assertEquals(90.0f, summary.getRate()); + + verify(singleAlertDao, times(1)).querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING); + verify(singleAlertDao, times(1)).count(); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertSilenceServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertSilenceServiceTest.java new file mode 100644 index 0000000..24b059a --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertSilenceServiceTest.java @@ -0,0 +1,127 @@ +/* + * 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.service; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Optional; +import java.util.Set; +import org.apache.hertzbeat.alert.dao.AlertSilenceDao; +import org.apache.hertzbeat.alert.service.impl.AlertSilenceServiceImpl; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; + +/** + * test case for {@link AlertSilenceServiceImpl} + */ + +class AlertSilenceServiceTest { + + @Mock + private AlertSilenceDao alertSilenceDao; + + @InjectMocks + private AlertSilenceServiceImpl alertSilenceService; + + @BeforeEach + void setUp() { + + MockitoAnnotations.openMocks(this); + + alertSilenceDao.save(AlertSilence + .builder() + .id(1L) + .type((byte) 1) + .build() + ); + + assertNotNull(alertSilenceDao.findAll()); + } + + @Test + void testValidate() { + + AlertSilence alertSilence = new AlertSilence(); + alertSilence.setType((byte) 1); + + alertSilenceService.validate(alertSilence, false); + + assertNotNull(alertSilence.getDays()); + assertEquals(7, alertSilence.getDays().size()); + } + + @Test + void testAddAlertSilence() { + + AlertSilence alertSilence = new AlertSilence(); + when(alertSilenceDao.save(any(AlertSilence.class))).thenReturn(alertSilence); + + assertDoesNotThrow(() -> alertSilenceService.addAlertSilence(alertSilence)); + verify(alertSilenceDao, times(1)).save(alertSilence); + } + + @Test + void testModifyAlertSilence() { + AlertSilence alertSilence = new AlertSilence(); + when(alertSilenceDao.save(any(AlertSilence.class))).thenReturn(alertSilence); + + assertDoesNotThrow(() -> alertSilenceService.modifyAlertSilence(alertSilence)); + verify(alertSilenceDao, times(1)).save(alertSilence); + } + + @Test + void testGetAlertSilence() { + AlertSilence alertSilence = new AlertSilence(); + when(alertSilenceDao.findById(anyLong())).thenReturn(Optional.of(alertSilence)); + + AlertSilence result = alertSilenceService.getAlertSilence(1L); + assertNotNull(result); + verify(alertSilenceDao, times(1)).findById(1L); + } + + @Test + void testGetAlertSilences() { + when(alertSilenceDao.findAll(any(Specification.class), any(PageRequest.class))).thenReturn(Page.empty()); + assertDoesNotThrow(() -> alertSilenceService.getAlertSilences(null, null, "id", "desc", 1, 10)); + verify(alertSilenceDao, times(1)).findAll(any(Specification.class), any(PageRequest.class)); + + assertNotNull(alertSilenceService.getAlertSilences(null, null, "id", "desc", 1, 10)); + } + + @Test + void testDeleteAlertSilences() { + + alertSilenceDao.deleteAlertSilencesByIdIn(Set.of(1L)); + + verify(alertSilenceDao, times(1)).deleteAlertSilencesByIdIn(Set.of(1L)); + } + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlibabaCloudSlsExternAlertServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlibabaCloudSlsExternAlertServiceTest.java new file mode 100644 index 0000000..1ef8917 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlibabaCloudSlsExternAlertServiceTest.java @@ -0,0 +1,185 @@ +/* + * 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.service; + + +import org.apache.hertzbeat.alert.dto.AlibabaCloudSlsExternAlert; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.impl.AlibabaCloudSlsExternAlertService; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.BeanUtils; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * unit test for {@link AlibabaCloudSlsExternAlertServiceTest } + */ +@ExtendWith(MockitoExtension.class) +public class AlibabaCloudSlsExternAlertServiceTest { + + @Mock + private AlarmCommonReduce alarmCommonReduce; + + @InjectMocks + private AlibabaCloudSlsExternAlertService externAlertService; + + @Test + void testAddExternAlertWithInvalidContent() { + String invalidContent = "invalid json"; + externAlertService.addExternAlert(invalidContent); + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testAddExternAlert() { + String source = externAlertService.supportSource(); + assertEquals("alibabacloud-sls", source); + + AlibabaCloudSlsExternAlert externAlert = new AlibabaCloudSlsExternAlert(); + externAlert.setAlertName("Test SLS alert"); + externAlert.setFireTime((int) Instant.now().getEpochSecond()); + externAlert.setAlertTime((int) Instant.now().getEpochSecond()); + externAlert.setRegion("cn-hangzhou"); + externAlert.setProject("project"); + externAlert.setStatus("firing"); + externAlert.setSeverity(AlibabaCloudSlsExternAlert.Severity.HIGH.getStatus()); + + Map<String, String> labels = new HashMap<>(); + labels.put("labels-k", "labels-v"); + externAlert.setLabels(labels); + + Map<String, String> annotations = new HashMap<>(); + annotations.put("annotations-k", "annotations-v"); + externAlert.setAnnotations(annotations); + + externAlertService.addExternAlert(JsonUtil.toJson(externAlert)); + + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testAddExternAlertWithSigninUrl() { + AlibabaCloudSlsExternAlert alert = new AlibabaCloudSlsExternAlert(); + alert.setAlertName("Test Alert"); + alert.setFireTime((int) Instant.now().getEpochSecond()); + alert.setAlertTime((int) Instant.now().getEpochSecond()); + alert.setRegion("cn-hangzhou"); + alert.setProject("test-project"); + alert.setStatus("firing"); + alert.setSeverity(AlibabaCloudSlsExternAlert.Severity.HIGH.getStatus()); + alert.setSigninUrl("https://example.com"); + externAlertService.addExternAlert(JsonUtil.toJson(alert)); + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testAddExternAlertWithDifferentSeverityLevels() { + for (AlibabaCloudSlsExternAlert.Severity severity : AlibabaCloudSlsExternAlert.Severity.values()) { + AlibabaCloudSlsExternAlert alert = new AlibabaCloudSlsExternAlert(); + alert.setAlertName("Test Alert"); + alert.setFireTime((int) Instant.now().getEpochSecond()); + alert.setAlertTime((int) Instant.now().getEpochSecond()); + alert.setRegion("cn-hangzhou"); + alert.setProject("test-project"); + alert.setStatus("firing"); + alert.setSeverity(severity.getStatus()); + externAlertService.addExternAlert(JsonUtil.toJson(alert)); + } + verify(alarmCommonReduce, times(AlibabaCloudSlsExternAlert.Severity.values().length)) + .reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testAddExternAlertWithEmptyLabelsAndAnnotations() { + AlibabaCloudSlsExternAlert alert = new AlibabaCloudSlsExternAlert(); + alert.setAlertName("Test Alert"); + alert.setFireTime((int) Instant.now().getEpochSecond()); + alert.setAlertTime((int) Instant.now().getEpochSecond()); + alert.setRegion("cn-hangzhou"); + alert.setProject("test-project"); + alert.setStatus("firing"); + alert.setSeverity(AlibabaCloudSlsExternAlert.Severity.HIGH.getStatus()); + alert.setLabels(new HashMap<>()); + alert.setAnnotations(new HashMap<>()); + externAlertService.addExternAlert(JsonUtil.toJson(alert)); + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testAddExternAlertWithInvalidSeverity() { + AlibabaCloudSlsExternAlert alert = new AlibabaCloudSlsExternAlert(); + alert.setAlertName("Test Alert"); + alert.setFireTime((int) Instant.now().getEpochSecond()); + alert.setAlertTime((int) Instant.now().getEpochSecond()); + alert.setRegion("cn-hangzhou"); + alert.setProject("test-project"); + alert.setStatus("firing"); + alert.setSeverity(-99); + externAlertService.addExternAlert(JsonUtil.toJson(alert)); + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testAddMergeExternAlert() { + String source = externAlertService.supportSource(); + assertEquals("alibabacloud-sls", source); + + AlibabaCloudSlsExternAlert externAlert = new AlibabaCloudSlsExternAlert(); + externAlert.setAlertName("Test SLS alert"); + externAlert.setFireTime((int) Instant.now().getEpochSecond()); + externAlert.setAlertTime((int) Instant.now().getEpochSecond()); + externAlert.setRegion("cn-hangzhou"); + externAlert.setProject("project"); + externAlert.setStatus("firing"); + externAlert.setSeverity(AlibabaCloudSlsExternAlert.Severity.HIGH.getStatus()); + + Map<String, String> labels = new HashMap<>(); + labels.put("labels-k", "labels-v"); + externAlert.setLabels(labels); + + Map<String, String> annotations = new HashMap<>(); + annotations.put("annotations-k", "annotations-v"); + externAlert.setAnnotations(annotations); + + AlibabaCloudSlsExternAlert externAlert1 = new AlibabaCloudSlsExternAlert(); + BeanUtils.copyProperties(externAlert, externAlert1); + + List<AlibabaCloudSlsExternAlert> externAlerts = new ArrayList<>(); + externAlerts.add(externAlert); + externAlerts.add(externAlert1); + externAlertService.addExternAlert(JsonUtil.toJson(externAlerts)); + verify(alarmCommonReduce, times(2)).reduceAndSendAlarm(any(SingleAlert.class)); + } + +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AwsSmsClientImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AwsSmsClientImplTest.java new file mode 100644 index 0000000..d8e9573 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AwsSmsClientImplTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service; + +import org.apache.hertzbeat.common.entity.dto.sms.AwsSmsProperties; +import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test case for {@link AwsSmsClientImpl} + */ +class AwsSmsClientImplTest { + + + @Test + void getType() { + AwsSmsProperties awsSmsProperties = new AwsSmsProperties(); + awsSmsProperties.setAccessKeyId("accessKeyId"); + awsSmsProperties.setAccessKeySecret("accessKeySecret"); + awsSmsProperties.setRegion("region"); + AwsSmsClientImpl awsSmsClient = new AwsSmsClientImpl(awsSmsProperties); + + assertEquals("aws", awsSmsClient.getType()); + } + + @ParameterizedTest + @CsvSource({ + "accessKeyId, accessKeySecret, region, true", + "accessKeyId, accessKeySecret, '', false", + "accessKeyId, '', region, false", + "accessKeyId, accessKeySecret, '', false", + "accessKeyId, '', '', false", + "'', accessKeySecret, region, false", + "'', accessKeySecret, '', false", + "'', '', region, false", + "'', accessKeySecret, '', false", + }) + void checkConfig(String accessKeyId, String accessKeySecret, String region, boolean expected) { + AwsSmsProperties awsSmsProperties = new AwsSmsProperties(); + awsSmsProperties.setAccessKeyId(accessKeyId); + awsSmsProperties.setAccessKeySecret(accessKeySecret); + awsSmsProperties.setRegion(region); + AwsSmsClientImpl awsSmsClient = new AwsSmsClientImpl(awsSmsProperties); + + assertEquals(expected, awsSmsClient.checkConfig()); + } + + + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java new file mode 100644 index 0000000..e9b098b --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java @@ -0,0 +1,790 @@ +/* + * 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.service; + +import com.github.benmanes.caffeine.cache.Cache; +import org.antlr.v4.runtime.tree.ParseTree; +import org.apache.hertzbeat.alert.service.impl.DataSourceServiceImpl; +import org.apache.hertzbeat.common.support.exception.AlertExpressionException; +import org.apache.hertzbeat.warehouse.db.QueryExecutor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * test case for {@link DataSourceService} + */ +class DataSourceServiceTest { + + private DataSourceServiceImpl dataSourceService; + + @BeforeEach + void setUp() { + dataSourceService = new DataSourceServiceImpl(null); + } + + @Test + void calculate1() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 150"); + assertEquals(2, result.size()); + assertNull(result.get(0).get("__value__")); + assertEquals(200.0, result.get(1).get("__value__")); + } + + @Test + void calculate2() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total <= 100"); + assertEquals(2, result.size()); + assertEquals(100.0, result.get(0).get("__value__")); + assertNull(result.get(1).get("__value__")); + } + + @Test + void calculate3() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total >= 200"); + assertEquals(2, result.size()); + assertNull(result.get(0).get("__value__")); + assertEquals(200.0, result.get(1).get("__value__")); + } + + @Test + void calculate4() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 250"); + assertEquals(2, result.size()); + assertNull(result.get(0).get("__value__")); + assertNull(result.get(1).get("__value__")); + } + + @Test + void calculate5() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total < 100"); + assertEquals(2, result.size()); + assertNull(result.get(0).get("__value__")); + assertNull(result.get(1).get("__value__")); + } + + @Test + void calculate6() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 200"); + assertEquals(2, result.size()); + assertNull(result.get(0).get("__value__")); + assertNull(result.get(1).get("__value__")); + } + + @Test + void calculate7() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "(node_cpu_seconds_total <= 100)"); + assertEquals(2, result.size()); + assertEquals(100.0, result.get(0).get("__value__")); + assertNull(result.get(1).get("__value__")); + } + + @Test + void calculate8() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"}[4m] <= 100"); + assertEquals(2, result.size()); + assertEquals(100.0, result.get(0).get("__value__")); + assertNull(result.get(1).get("__value__")); + } + + @Test + void calculate9() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} == 100"); + assertEquals(2, result.size()); + assertEquals(100.0, result.get(0).get("__value__")); + assertNull(result.get(1).get("__value__")); + } + + @Test + void calculate10() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} != 100"); + assertEquals(2, result.size()); + assertNull(result.get(0).get("__value__")); + assertEquals(200.0, result.get(1).get("__value__")); + } + + @Test + void calculate11() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 and node_cpu_seconds_total{mode=\"idle\"} < 120"); + assertEquals(1, result.size()); + assertEquals(100.0, result.get(0).get("__value__")); + } + + @Test + void calculate12() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "(node_cpu_seconds_total{mode=\"user\"} > 50) and (node_cpu_seconds_total{mode=\"idle\"} < 120)"); + assertEquals(1, result.size()); + assertEquals(100.0, result.get(0).get("__value__")); + } + + @Test + void calculate13() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 150 and node_cpu_seconds_total{mode=\"idle\"} < 220"); + assertEquals(1, result.size()); + assertEquals(200.0, result.get(0).get("__value__")); + } + + @Test + void calculate14() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "(node_cpu_seconds_total{mode=\"user\"} > 150) and (node_cpu_seconds_total{mode=\"idle\"} < 220)"); + assertEquals(1, result.size()); + assertEquals(200.0, result.get(0).get("__value__")); + } + + @Test + void calculate15() { + List<Map<String, Object>> prometheusData1 = List.of( + new HashMap<>(Map.of("__value__", 1)) + ); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute("count(node_cpu_seconds_total{mode=\"user\"} > 250)")).thenReturn(prometheusData1); + Mockito.when(mockExecutor.execute("count(node_cpu_seconds_total{mode=\"idle\"} < 220 )")).thenReturn(new ArrayList<>()); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "count(node_cpu_seconds_total{mode=\"user\"} > 250) > 0 and count(node_cpu_seconds_total{mode=\"idle\"} < 220 ) > 0"); + assertEquals(0, result.size()); + } + + @Test + void calculate16() { + List<Map<String, Object>> prometheusData1 = List.of( + new HashMap<>(Map.of("__value__", 1)) + ); + List<Map<String, Object>> prometheusData2 = List.of( + new HashMap<>(Map.of("__value__", 1)) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute("count(node_cpu_seconds_total{mode=\"user\"} > 250)")).thenReturn(prometheusData1); + Mockito.when(mockExecutor.execute("count(node_cpu_seconds_total{mode=\"idle\"} < 220 )")).thenReturn(prometheusData2); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "count(node_cpu_seconds_total{mode=\"user\"} > 250) > 0 and count(node_cpu_seconds_total{mode=\"idle\"} < 220 ) > 0"); + assertEquals(1, result.size()); + assertNotNull(result.get(0).get("__value__")); + } + + @Test + void calculate17() { + List<Map<String, Object>> prometheusData1 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + List<Map<String, Object>> prometheusData2 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 150 or node_cpu_seconds_total{mode=\"idle\"} < 20"); + assertEquals(1, result.size()); + assertEquals(200.0, result.get(0).get("__value__")); + } + + @Test + void calculate18() { + List<Map<String, Object>> prometheusData1 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + List<Map<String, Object>> prometheusData2 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 or node_cpu_seconds_total{mode=\"idle\"} < 120"); + assertEquals(1, result.size()); + assertEquals(100.0, result.get(0).get("__value__")); + } + + @Test + void calculate19() { + List<Map<String, Object>> prometheusData1 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + List<Map<String, Object>> prometheusData2 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 or node_cpu_seconds_total{mode=\"idle\"} < 20"); + assertEquals(0, result.size()); + } + + @Test + void calculate20() { + List<Map<String, Object>> prometheusData1 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + List<Map<String, Object>> prometheusData2 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "key", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "book", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 or node_cpu_seconds_total{mode=\"idle\"} < 320"); + assertEquals(4, result.size()); + } + + @Test + void calculate21() { + List<Map<String, Object>> prometheusData1 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + List<Map<String, Object>> prometheusData2 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "key", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "book", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 unless node_cpu_seconds_total{mode=\"idle\"} < 320"); + assertEquals(2, result.size()); + assertEquals(100.0, result.get(0).get("__value__")); + } + + @Test + void calculate22() { + List<Map<String, Object>> prometheusData1 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + List<Map<String, Object>> prometheusData2 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "key", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "book", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 unless node_cpu_seconds_total{mode=\"idle\"} < 20"); + assertEquals(2, result.size()); + assertEquals(100.0, result.get(0).get("__value__")); + } + + @Test + void calculate23() { + List<Map<String, Object>> prometheusData1 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + List<Map<String, Object>> prometheusData2 = List.of( + new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "key", "node1")), + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "book", "node2")) + ); + + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1); + Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 unless node_cpu_seconds_total{mode=\"idle\"} < 20"); + assertEquals(0, result.size()); + } + + @Test + void calculate24() { + List<Map<String, Object>> prometheusData = List.of(); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 150"); + assertEquals(0, result.size()); + } + + @Test + void calculate25() { + List<Map<String, Object>> prometheusData = List.of(); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 unless node_cpu_seconds_total{mode=\"idle\"} < 20"); + assertEquals(0, result.size()); + } + + @Test + void calculate26() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 150"); + assertEquals(1, result.size()); + assertEquals(200.0, result.get(0).get("__value__")); + } + + @Test + void expressionCacheShouldHitForSameExpression() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + String expr = "node_cpu_seconds_total > 50"; + Cache<String, ParseTree> expressionCache = dataSourceService.getExpressionCache(); + expressionCache.invalidateAll(); + long beforeHits = expressionCache.stats().hitCount(); + dataSourceService.calculate("promql", expr); //first execution miss cache + dataSourceService.calculate("promql", expr); //second execution hits cache + long actualHits = expressionCache.stats().hitCount() - beforeHits; + assertEquals(1, actualHits, "expression cache should hit but miss"); + } + + @Test + void tokenCacheShouldHitForSameExpression() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + String expr = "node_cpu_seconds_total > 50"; + Cache<String, ParseTree> expressionCache = dataSourceService.getExpressionCache(); + expressionCache.invalidateAll(); + long beforeHits = expressionCache.stats().hitCount(); + dataSourceService.calculate("promql", expr); + dataSourceService.calculate("promql", expr); + long actualHits = expressionCache.stats().hitCount() - beforeHits; + assertEquals(1, actualHits, "expression cache should hit but miss"); + } + + @Test + void expressCacheShouldNotHitForDifferentExpressions() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + dataSourceService.calculate("promql", "node_cpu_seconds_total > 50"); + Cache<String, ParseTree> expressionCache = dataSourceService.getExpressionCache(); + expressionCache.invalidateAll(); + String expr1 = "node_cpu_seconds_total > 30"; + String expr2 = "node_cpu_seconds_total > 50"; + long beforeHits = expressionCache.stats().hitCount(); + dataSourceService.calculate("promql", expr1); + dataSourceService.calculate("promql", expr2); + long actualHits = expressionCache.stats().hitCount() - beforeHits; + assertEquals(0, actualHits, "expression cache should miss but hit"); + } + + @Test + void tokenCacheShouldNotHitForDifferentExpressions() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2")) + ); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + Mockito.when(mockExecutor.support("promql")).thenReturn(true); + Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + dataSourceService.calculate("promql", "node_cpu_seconds_total > 50"); + Cache<String, ParseTree> expressionCache = dataSourceService.getExpressionCache(); + expressionCache.invalidateAll(); + String expr1 = "node_cpu_seconds_total > 30"; + String expr2 = "node_cpu_seconds_total > 50"; + long beforeHits = expressionCache.stats().hitCount(); + dataSourceService.calculate("promql", expr1); + dataSourceService.calculate("promql", expr2); + long actualHits = expressionCache.stats().hitCount() - beforeHits; + assertEquals(0, actualHits, "expression cache should miss but hit"); + } + + @Test + void testAlertExpressionException() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>() { + { + put("exception", "none"); + put("instance", "host.docker.internal:8989"); + put("__value__", 1307); + put("method", "GET"); + put("__name__", "http_server_requests_seconds_count"); + put("__timestamp__", "1.750320922467E9"); + put("error", "none"); + put("job", "spring-boot-app"); + put("uri", "/actuator/prometheus"); + put("outcome", "SUCCESS"); + put("status", "200"); + } + }); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support(eq("promql"))).thenReturn(true); + when(mockExecutor.execute(eq("http_server_requests_seconds_count"))).thenReturn(prometheusData); + + dataSourceService.setExecutors(List.of(mockExecutor)); + List<Map<String, Object>> result = dataSourceService.calculate("promql", "http_server_requests_seconds_count > 10"); + assertNotNull(result); + assertEquals(1307, result.get(0).get("__value__")); + + assertThrows(AlertExpressionException.class, () -> dataSourceService.calculate("promql", "http_server_requests_seconds_count{!@~!!#$%^&}")); + } + + @Test + void query1() { + List<Map<String, Object>> sqlData = List.of( + new HashMap<>(Map.of("count", 10, "severity_text", "ERROR")) + ); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("sql")).thenReturn(true); + when(mockExecutor.execute(anyString())).thenReturn(sqlData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + String validSql = "SELECT count(*) FROM hertzbeat_logs WHERE severity_text = 'ERROR'"; + List<Map<String, Object>> result = dataSourceService.query("sql", validSql); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(mockExecutor).execute(anyString()); + } + + @Test + void query2() { + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("sql")).thenReturn(true); + dataSourceService.setExecutors(List.of(mockExecutor)); + + assertThrows(AlertExpressionException.class, + () -> dataSourceService.query("sql", "INSERT INTO hertzbeat_logs (body) VALUES ('test')")); + verify(mockExecutor, never()).execute(anyString()); + } + + @Test + void query3() { + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("sql")).thenReturn(true); + dataSourceService.setExecutors(List.of(mockExecutor)); + + assertThrows(AlertExpressionException.class, + () -> dataSourceService.query("sql", "DELETE FROM hertzbeat_logs WHERE id = 1")); + verify(mockExecutor, never()).execute(anyString()); + } + + @Test + void query4() { + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("sql")).thenReturn(true); + dataSourceService.setExecutors(List.of(mockExecutor)); + + assertThrows(AlertExpressionException.class, + () -> dataSourceService.query("sql", "UPDATE hertzbeat_logs SET body = 'hacked' WHERE id = 1")); + verify(mockExecutor, never()).execute(anyString()); + } + + @Test + void query5() { + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("sql")).thenReturn(true); + dataSourceService.setExecutors(List.of(mockExecutor)); + + assertThrows(AlertExpressionException.class, + () -> dataSourceService.query("sql", "DROP TABLE hertzbeat_logs")); + verify(mockExecutor, never()).execute(anyString()); + } + + @Test + void query6() { + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("sql")).thenReturn(true); + dataSourceService.setExecutors(List.of(mockExecutor)); + + assertThrows(AlertExpressionException.class, + () -> dataSourceService.query("sql", "SELECT * FROM hertzbeat_logs UNION SELECT * FROM users")); + verify(mockExecutor, never()).execute(anyString()); + } + + @Test + void query7() { + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("sql")).thenReturn(true); + dataSourceService.setExecutors(List.of(mockExecutor)); + + assertThrows(AlertExpressionException.class, + () -> dataSourceService.query("sql", "SELECT * FROM hertzbeat_logs WHERE id IN (SELECT id FROM other_table)")); + verify(mockExecutor, never()).execute(anyString()); + } + + @Test + void query8() { + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("sql")).thenReturn(true); + dataSourceService.setExecutors(List.of(mockExecutor)); + + assertThrows(AlertExpressionException.class, + () -> dataSourceService.query("sql", "SELECT * FROM users")); + verify(mockExecutor, never()).execute(anyString()); + } + + @Test + void query9() { + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("sql")).thenReturn(true); + dataSourceService.setExecutors(List.of(mockExecutor)); + + assertThrows(AlertExpressionException.class, + () -> dataSourceService.query("sql", "WITH cte AS (SELECT * FROM hertzbeat_logs) SELECT * FROM cte")); + verify(mockExecutor, never()).execute(anyString()); + } + + @Test + void query10() { + List<Map<String, Object>> sqlData = List.of( + new HashMap<>(Map.of("errorCount", 5)) + ); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("sql")).thenReturn(true); + when(mockExecutor.execute(anyString())).thenReturn(sqlData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + String complexSql = "SELECT count(*) AS errorCount FROM hertzbeat_logs " + + "WHERE time_unix_nano >= NOW() AND severity_text = 'ERROR' " + + "GROUP BY severity_text HAVING count(*) > 2 ORDER BY errorCount LIMIT 10"; + + List<Map<String, Object>> result = dataSourceService.query("sql", complexSql); + assertNotNull(result); + verify(mockExecutor).execute(anyString()); + } + + @Test + void query11() { + List<Map<String, Object>> prometheusData = List.of( + new HashMap<>(Map.of("__value__", 100.0)) + ); + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("promql")).thenReturn(true); + when(mockExecutor.execute(anyString())).thenReturn(prometheusData); + dataSourceService.setExecutors(List.of(mockExecutor)); + + List<Map<String, Object>> result = dataSourceService.query("promql", "node_cpu_seconds_total > 50"); + + assertNotNull(result); + verify(mockExecutor).execute(anyString()); + } + + @Test + void query12() { + QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class); + when(mockExecutor.support("sql")).thenReturn(true); + dataSourceService.setExecutors(List.of(mockExecutor)); + + assertThrows(AlertExpressionException.class, + () -> dataSourceService.query("sql", "SELEC * FORM hertzbeat_logs")); + verify(mockExecutor, never()).execute(anyString()); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java new file mode 100644 index 0000000..3859303 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java @@ -0,0 +1,187 @@ +/* + * 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.service; + +import org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.impl.HuaweiCloudExternAlertService; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * unit test for {@link AlibabaCloudSlsExternAlertServiceTest } + */ +@ExtendWith(MockitoExtension.class) +public class HuaweiCloudExternAlertServiceTest { + + @Mock + private AlarmCommonReduce alarmCommonReduce; + + @InjectMocks + private HuaweiCloudExternAlertService externAlertService; + + @BeforeEach + void setUp() { + assertEquals("huaweicloud-ces", externAlertService.supportSource()); + } + + @Test + void testAddExternAlertWithInvalidContent() { + String invalidContent = "invalid json"; + externAlertService.addExternAlert(invalidContent); + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testMessageValidFailed() { + HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert(); + externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1"); + externAlert.setType("failedType"); + externAlert.setMessage("failedMessage"); + externAlert.setTimestamp("2025-06-07T15:12:09Z"); + externAlertService.addExternAlert(JsonUtil.toJson(externAlert)); + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testCertFailed() { + HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert(); + externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLb" + + "vaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwX" + + "w1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9lYyeL7vVI0e4sfRbuQruz3E" + + "0ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw=="); + externAlert.setSubject("[华为云][紧急告警恢复]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已恢复正常。"); + externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX"); + externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1"); + externAlert.setType("Notification"); + externAlert.setMessage("{}"); + externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/failedUrl"); + externAlert.setTimestamp("2025-06-07T15:12:09Z"); + externAlertService.addExternAlert(JsonUtil.toJson(externAlert)); + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testAddExternAlert() { + HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert(); + externAlert.setSignature("Igs0bBhzw0JGmlgBH+9ejw2xWfPTXjAatAEsKDkkWcC5bZ/jveckdRZdgp/S0JER9eiJfMF427YDABufIN0sv/vBRXaRQKfRBLTJYbSTl+AQpEbIW5yUfJSRLEG3HNEhUDjASolbrW7zdPCoGkkqjifE23FCvw" + + "+4tewMzqmHnfJHcFBq3W89CJzdPBjwO1UcY9C39moUZgqZk+qDVLpxb4bHSrEYAwPOSrOPR7TZpETJ30UOgFYajJydQk692edfs0NeVutHoQiOJ5/YC83ULHft0aXhichjtfZE4KF69nROAKez0ubk3l" + + "Ey/mBIM9Ylbxn5b84OIrzzZQrIWe8Syw=="); + externAlert.setSubject("[华为云][紧急告警]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已触发告警。"); + externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX"); + externAlert.setMessageId("1565df032a19494590d61e05f7b0dc0e"); + externAlert.setType("Notification"); + externAlert.setMessage("{\"version\":\"v1\",\"data\":{\"AccountName\":\"hid_hk6tij5o1v-95zn\",\"Namespace\":\"分布式缓存服务\",\"DimensionName\":\"DCS Redis实例\",\"ResourceName\"" + + ":\"dcs-h4tv\",\"MetricName\":\"每秒并发操作数\",\"IsAlarm\":true,\"AlarmLevel\":\"紧急\",\"Region\":\"华东-上海一\",\"RegionId\":\"cn-east-3\",\"ResourceId\":\"3dc7b9ea" + + "-70b4-4c38-942d-e2636e6d844c\",\"PrivateIp\":\"192.168.0.54\",\"CurrentData\":\"6.00 count\",\"AlarmTime\":\"2025/06/02 22:56:15 GMT+08:00\"," + + "\"AlarmRecordID\":\"ah1748876175242njvndyzMZ\"," + + "\"AlarmRuleName\":\"alarm-c5jj\",\"IsOriginalValue\":true,\"Filter\":\"原始值\",\"ComparisonOperator\":\"\\u003e\",\"Value\":\"5 count\",\"Unit\":\"count\",\"Count\":2}}"); + externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem"); + externAlert.setTimestamp("2025-06-02T14:56:17Z"); + externAlertService.addExternAlert(JsonUtil.toJson(externAlert)); + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testSubscriptionUrl() { + HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert(); + externAlert.setSubscribeUrl("https://console.huaweicloud.com/smn/subscription/confirm?token=477a784601d744e4ab9ab83986502d31c4b938" + + "0ec0b64392b134e517c3aa17eb7b3a12dc9f3b4ab495e61c4dee654b435d7223ea934345bf8ae8901cef912b1d&topic_urn=urn:smn:cn-north-4" + + ":477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX®ion=cn-north-4"); + externAlert.setSignature("ottf37C/2RdDgqimRQMIBU6i7XjUfPPMU760jJn71wwP3825YPoIT22uw2A9399rkm9Jrt1qUEFrDLuA5yHFLd5n/XoM4FghIgyFn7VIfgpuVM31a+co78s" + + "YBiZ1egOCE/AwFm2oygRhfIceUj9Kw9vmc06el9TXY6RtE5tAEF6qEmICtTh45KwtCO/WRs3DY72dQi5hm0w7/tktS4WFZ1iP4LHt5eCwFvnH0u29Y96cJNI0fLUQxI5MkhgjK" + + "77JkFK7UT6ZYJZhzgSp/B7OQGStOQx+3Duvx4T4CzccZQM3sca81Z0B0GFGWeVXuEHyCPLsayY/Iz+5Tco51elT8w=="); + externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX"); + externAlert.setMessageId("242fac183d3a4936b5ead6c725a32ed0"); + externAlert.setType("SubscriptionConfirmation"); + externAlert.setMessage("You are invited to subscribe to topic: urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:" + + "CES_notification_group_bngJ2aMpX. To confirm this subscription, please visit the subscribe_url included in this message. The subscribe_url is valid only within 48 hours."); + externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem"); + externAlert.setTimestamp("2025-06-07T15:07:14Z"); + externAlertService.addExternAlert(JsonUtil.toJson(externAlert)); + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testUnsubscribe() { + HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert(); + externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLbvaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np" + + "1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwXw1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9l" + + "YyeL7vVI0e4sfRbuQruz3E0+ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw=="); + externAlert.setSubject("[华为云][紧急告警恢复]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已恢复正常。"); + externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX"); + externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1"); + externAlert.setType("UnsubscribeConfirmation"); + externAlert.setMessage("{}"); + externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem"); + externAlert.setTimestamp("2025-06-07T15:12:09Z"); + externAlertService.addExternAlert(JsonUtil.toJson(externAlert)); + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testSubscriptionUrlFail() { + HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert(); + externAlert.setSubscribeUrl("https://console.huaweicloud.com/smn/subscription/confirm?token=477a784601d744e4ab9ab83986502d31c4b938" + + "0ec0b64392b134e517c3aa17eb7b3a12dc9f3b4ab495e61c4dee654b435d7223ea934345bf8ae8901cef912b1d&topic_urn=urn:smn:cn-north-4" + + ":477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX®ion=cn-north-4"); + externAlert.setSignature("ottf37C/2RdDgqimRQMIBU6i7XjUfPPMU760jJn71wwP3825YPoIT22uw2A9399rkm9Jrt1qUEFrDLuA5yHFLd5n/XoM4FghIgyFn7VIfgpuVM31a+co78s" + + "YBiZ1egOCE/AwFm2oygRhfIceUj9Kw9vmc06el9TXY6RtE5tAEF6qEmICtTh45KwtCO/WRs3DY72dQi5hm0w7/tktS4WFZ1iP4LHt5eCwFvnH0u29Y96cJNI0fLUQxI5MkhgjK" + + "77JkFK7UT6ZYJZhzgSp/B7OQGStOQx+3Duvx4T4CzccZQM3sca81Z0B0GFGWeVXuEHyCPLsayY/Iz+5Tco51elT8w=="); + externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX"); + externAlert.setMessageId("242fac183d3a4936b5ead6c725a32ed0"); + externAlert.setType("SubscriptionConfirmation"); + externAlert.setMessage("You are invited to subscribe to topic: urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:" + + "CES_notification_group_bngJ2aMpX. To confirm this subscription, please visit the subscribe_url included in this message. The subscribe_url is valid only within 48 hours."); + externAlert.setSigningCertUrl("https://xxxx.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem"); + externAlert.setTimestamp("2025-06-07T15:07:14Z"); + externAlertService.addExternAlert(JsonUtil.toJson(externAlert)); + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testCertUrlFailed() { + HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert(); + externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLb" + + "vaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwX" + + "w1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9lYyeL7vVI0e4sfRbuQruz3E" + + "0ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw=="); + externAlert.setSubject("DCS Redis “dcs-h4tv” "); + externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX"); + externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1"); + externAlert.setType("Notification"); + externAlert.setMessage("{}"); + externAlert.setSigningCertUrl("https://xxx.myhuaweicloud.com/failedUrl"); + externAlert.setTimestamp("2025-06-07T15:12:09Z"); + externAlertService.addExternAlert(JsonUtil.toJson(externAlert)); + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + } + +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java new file mode 100644 index 0000000..81a9c2c --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java @@ -0,0 +1,382 @@ +/* + * 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.service; + +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.notice.AlertNoticeDispatch; +import org.apache.hertzbeat.alert.service.impl.NoticeConfigServiceImpl; +import org.apache.hertzbeat.common.cache.CacheFactory; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +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.common.entity.alerter.SingleAlert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +/** + * Test case for {@link NoticeConfigService} + */ +@ExtendWith(MockitoExtension.class) +class NoticeConfigServiceTest { + + @Mock + NoticeReceiverDao noticeReceiverDao; + @Mock + NoticeTemplateDao noticeTemplateDao; + @Mock + NoticeRuleDao noticeRuleDao; + @Mock + AlertNoticeDispatch dispatcherAlarm; + @InjectMocks + private NoticeConfigServiceImpl noticeConfigService; + + private NoticeReceiver receiver1; + private NoticeReceiver receiver2; + private NoticeTemplate template1; + private NoticeTemplate template2; + private NoticeRule rule1; + private NoticeRule rule2; + + @BeforeEach + void setUp() { + receiver1 = new NoticeReceiver(); + receiver1.setId(1L); + receiver1.setName("Receiver1"); + + receiver2 = new NoticeReceiver(); + receiver2.setId(2L); + receiver2.setName("Receiver2"); + + template1 = new NoticeTemplate(); + template1.setId(1L); + template1.setName("Template1"); + + template2 = new NoticeTemplate(); + template2.setId(2L); + template2.setName("Template2"); + + rule1 = new NoticeRule(); + rule1.setId(1L); + rule1.setName("Rule1"); + + rule2 = new NoticeRule(); + rule2.setId(2L); + rule2.setName("Rule2"); + } + + @Test + void getNoticeReceivers() { + Page<NoticeReceiver> receiverPage = new PageImpl<>( + Arrays.asList(receiver1, receiver2), + PageRequest.of(0, 8, Sort.by(Sort.Direction.DESC, "id")), + 2 + ); + + when(noticeReceiverDao.findAll(any(Specification.class), any(PageRequest.class))).thenReturn(receiverPage); + + Page<NoticeReceiver> result = noticeConfigService.getNoticeReceivers("Receiver", 0, 8); + + assertEquals(2, result.getTotalElements()); + assertEquals(1, result.getTotalPages()); + assertEquals(8, result.getSize()); + assertEquals(0, result.getNumber()); + assertEquals(receiver1, result.getContent().get(0)); + assertEquals(receiver2, result.getContent().get(1)); + + verify(noticeReceiverDao, times(1)).findAll(any(Specification.class), any(PageRequest.class)); + } + + @Test + void getAllNoticeReceivers() { + when(noticeReceiverDao.findAll()).thenReturn(Arrays.asList(receiver1, receiver2)); + + List<NoticeReceiver> result = noticeConfigService.getAllNoticeReceivers(); + + assertEquals(2, result.size()); + assertEquals(receiver1, result.get(0)); + assertEquals(receiver2, result.get(1)); + + verify(noticeReceiverDao, times(1)).findAll(); + } + + @Test + void getNoticeTemplates() { + Page<NoticeTemplate> templatePage = new PageImpl<>( + Arrays.asList(template1, template2), + PageRequest.of(0, 8, Sort.by(Sort.Direction.DESC, "id")), + 2 + ); + + when(noticeTemplateDao.findAll(any(Specification.class), any(PageRequest.class))).thenReturn(templatePage); + + Page<NoticeTemplate> result = noticeConfigService.getNoticeTemplates("Template", false, 0, 8); + + assertEquals(2, result.getTotalElements()); + assertEquals(1, result.getTotalPages()); + assertEquals(8, result.getSize()); + assertEquals(0, result.getNumber()); + assertEquals(template1, result.getContent().get(0)); + assertEquals(template2, result.getContent().get(1)); + + verify(noticeTemplateDao, times(1)).findAll(any(Specification.class), any(PageRequest.class)); + } + + @Test + void getAllNoticeTemplates() { + when(noticeTemplateDao.findAll()).thenReturn(Arrays.asList(template1, template2)); + + List<NoticeTemplate> result = noticeConfigService.getAllNoticeTemplates(); + + assert result.size() >= 2; + assertEquals(template1, result.get(result.size() - 2)); + assertEquals(template2, result.get(result.size() - 1)); + + verify(noticeTemplateDao, times(1)).findAll(); + } + + @Test + void getNoticeRules() { + Page<NoticeRule> rulePage = new PageImpl<>( + Arrays.asList(rule1, rule2), + PageRequest.of(0, 8, Sort.by(Sort.Direction.DESC, "id")), + 2 + ); + + when(noticeRuleDao.findAll(any(Specification.class), any(PageRequest.class))).thenReturn(rulePage); + + Page<NoticeRule> result = noticeConfigService.getNoticeRules("Rule", 0, 8); + + assertEquals(2, result.getTotalElements()); + assertEquals(1, result.getTotalPages()); + assertEquals(8, result.getSize()); + assertEquals(0, result.getNumber()); + assertEquals(rule1, result.getContent().get(0)); + assertEquals(rule2, result.getContent().get(1)); + + verify(noticeRuleDao, times(1)).findAll(any(Specification.class), any(PageRequest.class)); + } + + @Test + void addReceiver() { + final NoticeReceiver noticeReceiver = mock(NoticeReceiver.class); + noticeConfigService.addReceiver(noticeReceiver); + verify(noticeReceiverDao, times(1)).save(noticeReceiver); + } + + @Test + void editReceiver() { + final NoticeReceiver noticeReceiver = mock(NoticeReceiver.class); + noticeConfigService.editReceiver(noticeReceiver); + verify(noticeReceiverDao, times(1)).save(noticeReceiver); + } + + @Test + void deleteReceiver() { + final Long receiverId = 23342525L; + noticeConfigService.deleteReceiver(receiverId); + verify(noticeReceiverDao, times(1)).deleteById(receiverId); + } + + @Test + void addTemplate() { + final NoticeTemplate noticeTemplate = mock(NoticeTemplate.class); + noticeConfigService.addNoticeTemplate(noticeTemplate); + verify(noticeTemplateDao, times(1)).save(noticeTemplate); + } + + @Test + void editTemplate() { + final NoticeTemplate noticeTemplate = mock(NoticeTemplate.class); + noticeConfigService.editNoticeTemplate(noticeTemplate); + verify(noticeTemplateDao, times(1)).save(noticeTemplate); + } + + @Test + void deleteTemplate() { + final Long templateId = 23342525L; + noticeConfigService.deleteNoticeTemplate(templateId); + verify(noticeTemplateDao, times(1)).deleteById(templateId); + } + + @Test + void addNoticeRule() { + final NoticeRule noticeRule = mock(NoticeRule.class); + noticeConfigService.addNoticeRule(noticeRule); + verify(noticeRuleDao, times(1)).save(noticeRule); + } + + @Test + void editNoticeRule() { + final NoticeRule noticeRule = mock(NoticeRule.class); + noticeConfigService.editNoticeRule(noticeRule); + verify(noticeRuleDao, times(1)).save(noticeRule); + } + + @Test + void deleteNoticeRule() { + final Long ruleId = 23342525L; + noticeConfigService.deleteNoticeRule(ruleId); + verify(noticeRuleDao, times(1)).deleteById(ruleId); + } + + @Test + void getReceiverById() { + final Long receiverId = 343432325L; + noticeConfigService.getReceiverById(receiverId); + verify(noticeReceiverDao, times(1)).findById(receiverId); + } + + @Test + void getNoticeRulesById() { + final Long receiverId = 343432325L; + noticeConfigService.getNoticeRulesById(receiverId); + verify(noticeRuleDao, times(1)).findById(receiverId); + } + + @Test + void getNoticeTemplateById() { + final Long templateId = 343432325L; + noticeConfigService.getNoticeTemplatesById(templateId); + verify(noticeTemplateDao, times(1)).findById(templateId); + } + + @Test + void sendTestMsg() { + final NoticeReceiver noticeReceiver = mock(NoticeReceiver.class); + final NoticeTemplate noticeTemplate = null; + noticeConfigService.sendTestMsg(noticeReceiver); + verify(dispatcherAlarm, times(1)).sendNoticeMsg(eq(noticeReceiver), eq(noticeTemplate), any(GroupAlert.class)); + } + + @AfterEach + void tearDown() { + CacheFactory.clearNoticeCache(); + } + + private GroupAlert buildGroupAlertWithPartialLabel() { + SingleAlert alertA = SingleAlert.builder() + .labels(new HashMap<>(Map.of("alertname", "cpu", "instance", "s1", "department", "algorithm"))) + .build(); + SingleAlert alertB = SingleAlert.builder() + .labels(new HashMap<>(Map.of("alertname", "cpu", "instance", "s2"))) + .build(); + return GroupAlert.builder() + .commonLabels(new HashMap<>(Map.of("alertname", "cpu"))) + .alerts(Arrays.asList(alertA, alertB)) + .build(); + } + + /** + * A rule scoped to a label carried by only part of the group's alerts must still match, + * even though that label is absent from commonLabels. Regression test for issue #3852. + */ + @Test + void getReceiverFilterRuleMatchesLabelOnPartialAlert() { + NoticeRule rule = new NoticeRule(); + rule.setId(1L); + rule.setName("algorithm-team"); + rule.setFilterAll(false); + rule.setLabels(new HashMap<>(Map.of("department", "algorithm"))); + CacheFactory.setNoticeCache(List.of(rule)); + + List<NoticeRule> matched = noticeConfigService.getReceiverFilterRule(buildGroupAlertWithPartialLabel()); + + assertEquals(1, matched.size()); + assertEquals(1L, matched.get(0).getId()); + } + + /** + * A rule whose label value is not present on any alert in the group must not match. + */ + @Test + void getReceiverFilterRuleSkipsRuleWithUnmatchedLabel() { + NoticeRule rule = new NoticeRule(); + rule.setId(2L); + rule.setName("infra-team"); + rule.setFilterAll(false); + rule.setLabels(new HashMap<>(Map.of("department", "infra"))); + CacheFactory.setNoticeCache(List.of(rule)); + + List<NoticeRule> matched = noticeConfigService.getReceiverFilterRule(buildGroupAlertWithPartialLabel()); + + assertTrue(matched.isEmpty()); + } + + /** + * A rule requiring several labels matches only when a single alert carries all of them, + * preventing false matches assembled across different alerts in the group. + */ + @Test + void getReceiverFilterRuleRequiresAllLabelsOnSameAlert() { + NoticeRule rule = new NoticeRule(); + rule.setId(3L); + rule.setName("algorithm-on-s2"); + rule.setFilterAll(false); + rule.setLabels(new HashMap<>(Map.of("department", "algorithm", "instance", "s2"))); + CacheFactory.setNoticeCache(List.of(rule)); + + List<NoticeRule> matched = noticeConfigService.getReceiverFilterRule(buildGroupAlertWithPartialLabel()); + + assertTrue(matched.isEmpty()); + } + + /** + * A forward-all rule keeps matching regardless of labels. + */ + @Test + void getReceiverFilterRuleForwardAllAlwaysMatches() { + NoticeRule rule = new NoticeRule(); + rule.setId(4L); + rule.setName("forward-all"); + rule.setFilterAll(true); + CacheFactory.setNoticeCache(List.of(rule)); + + List<NoticeRule> matched = noticeConfigService.getReceiverFilterRule(buildGroupAlertWithPartialLabel()); + + assertEquals(1, matched.size()); + assertEquals(4L, matched.get(0).getId()); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/SmsClientFactoryTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/SmsClientFactoryTest.java new file mode 100644 index 0000000..5a7a0fe --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/SmsClientFactoryTest.java @@ -0,0 +1,86 @@ +/* + * 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.service; + +import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig; +import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum; +import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.when; + + +/** + * unit test for {@link SmsClientFactory } + */ +@ExtendWith(MockitoExtension.class) +public class SmsClientFactoryTest { + + + @Mock + private GeneralConfigDao generalConfigDao; + + @Mock + private SmsConfig yamlSmsConfig; + + @InjectMocks + private SmsClientFactory smsClientFactory; + + + @Test + void testloadDbConfig() { + GeneralConfig generalConfig = new GeneralConfig(); + + SmsConfig smsConfig = new SmsConfig(); + smsConfig.setType("smslocal"); + smsConfig.setEnable(true); + smsConfig.setSmslocal(new SmslocalSmsProperties("11")); + + generalConfig.setContent(JsonUtil.toJson(smsConfig)); + when(generalConfigDao.findByType(GeneralConfigTypeEnum.sms.name())).thenReturn(generalConfig); + + assertNotNull(smsClientFactory.getSmsClient()); + + } + + @Test + void testloadYamlConfig() { + when(generalConfigDao.findByType(GeneralConfigTypeEnum.sms.name())).thenReturn(null); + when(yamlSmsConfig.getType()).thenReturn("smslocal"); + when(yamlSmsConfig.isEnable()).thenReturn(true); + when(yamlSmsConfig.getSmslocal()).thenReturn(new SmslocalSmsProperties("11")); + assertNotNull(smsClientFactory.getSmsClient()); + } + + @Test + void testNull() { + when(generalConfigDao.findByType(GeneralConfigTypeEnum.sms.name())).thenReturn(null); + when(yamlSmsConfig.getType()).thenReturn(""); + assertNull(smsClientFactory.getSmsClient()); + } + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/TwilioSmsClientImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/TwilioSmsClientImplTest.java new file mode 100644 index 0000000..ef4f47c --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/TwilioSmsClientImplTest.java @@ -0,0 +1,64 @@ +/* + * 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.service; + +import org.apache.hertzbeat.common.entity.dto.sms.TwilioSmsProperties; +import org.apache.hertzbeat.alert.service.impl.TwilioSmsClientImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test case for {@link TwilioSmsClientImpl} + */ +class TwilioSmsClientImplTest { + + @Test + void getType() { + TwilioSmsProperties twilioSmsProperties = new TwilioSmsProperties(); + twilioSmsProperties.setAccountSid("accountSid"); + twilioSmsProperties.setAuthToken("authToken"); + twilioSmsProperties.setTwilioPhoneNumber("twilioPhoneNumber"); + TwilioSmsClientImpl twilioSmsClient = new TwilioSmsClientImpl(twilioSmsProperties); + + assertEquals("twilio", twilioSmsClient.getType()); + } + + @ParameterizedTest + @CsvSource({ + "accountSid, authToken, twilioPhoneNumber, true", + "accountSid, authToken, '', false", + "accountSid, '', twilioPhoneNumber, false", + "accountSid, '', '', false", + "'', authToken, twilioPhoneNumber, false", + "'', authToken, '', false", + "'', '', twilioPhoneNumber, false", + "'', '', '', false", + }) + void checkConfig(String accountSid, String authToken, String twilioPhoneNumber, boolean expected) { + TwilioSmsProperties twilioSmsProperties = new TwilioSmsProperties(); + twilioSmsProperties.setAccountSid(accountSid); + twilioSmsProperties.setAuthToken(authToken); + twilioSmsProperties.setTwilioPhoneNumber(twilioPhoneNumber); + TwilioSmsClientImpl twilioSmsClient = new TwilioSmsClientImpl(twilioSmsProperties); + + assertEquals(expected, twilioSmsClient.checkConfig()); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImplTest.java new file mode 100644 index 0000000..a0739e2 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImplTest.java @@ -0,0 +1,115 @@ +/* + * 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.service.impl; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; +import java.util.Collections; +import java.util.List; +import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao; +import org.apache.hertzbeat.alert.reduce.AlarmGroupReduce; +import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link AlertGroupConvergeServiceImpl} + */ +@ExtendWith(MockitoExtension.class) +public class AlertGroupConvergeServiceImplTest { + + @Mock + private AlertGroupConvergeDao alertGroupConvergeDao; + + @Mock + private AlarmGroupReduce alarmGroupReduce; + + @InjectMocks + private AlertGroupConvergeServiceImpl alertGroupConvergeService; + + private AlertGroupConverge converge; + + @BeforeEach + void setUp() { + converge = AlertGroupConverge.builder() + .id(1L) + .name("test-converge") + .build(); + } + + @Test + void testValidateNewSuccess() { + converge.setId(null); + when(alertGroupConvergeDao.findAlertGroupConvergesByName("test-converge")) + .thenReturn(Collections.emptyList()); + assertDoesNotThrow(() -> alertGroupConvergeService.validate(converge, false)); + } + + @Test + void testValidateNewDuplicateName() { + converge.setId(null); + AlertGroupConverge existing = AlertGroupConverge.builder() + .id(2L) + .name("test-converge") + .build(); + when(alertGroupConvergeDao.findAlertGroupConvergesByName("test-converge")) + .thenReturn(List.of(existing)); + assertThrows(IllegalArgumentException.class, () -> alertGroupConvergeService.validate(converge, false)); + } + + @Test + void testValidateNewEmptyName() { + converge.setName(""); + assertThrows(IllegalArgumentException.class, () -> alertGroupConvergeService.validate(converge, false)); + + converge.setName(null); + assertThrows(IllegalArgumentException.class, () -> alertGroupConvergeService.validate(converge, false)); + } + + @Test + void testValidateModifySuccessNameUnchanged() { + when(alertGroupConvergeDao.findAlertGroupConvergesByName("test-converge")) + .thenReturn(List.of(converge)); + assertDoesNotThrow(() -> alertGroupConvergeService.validate(converge, true)); + } + + @Test + void testValidateModifySuccessNameChanged() { + converge.setName("new-name"); + when(alertGroupConvergeDao.findAlertGroupConvergesByName("new-name")) + .thenReturn(Collections.emptyList()); + assertDoesNotThrow(() -> alertGroupConvergeService.validate(converge, true)); + } + + @Test + void testValidateModifyDuplicateName() { + converge.setName("existing-name"); + AlertGroupConverge existing = AlertGroupConverge.builder() + .id(2L) + .name("existing-name") + .build(); + when(alertGroupConvergeDao.findAlertGroupConvergesByName("existing-name")) + .thenReturn(List.of(existing)); + assertThrows(IllegalArgumentException.class, () -> alertGroupConvergeService.validate(converge, true)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/AlertManagerExternAlertServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/AlertManagerExternAlertServiceTest.java new file mode 100644 index 0000000..c2966b9 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/AlertManagerExternAlertServiceTest.java @@ -0,0 +1,288 @@ +/* + * 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.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.hertzbeat.alert.dto.AlertManagerExternAlert; +import org.apache.hertzbeat.alert.dto.PrometheusExternAlert; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * unit test for {@link AlertManagerExternAlertService} + */ +@ExtendWith(MockitoExtension.class) +public class AlertManagerExternAlertServiceTest { + + @Mock + private AlarmCommonReduce alarmCommonReduce; + + @InjectMocks + private AlertManagerExternAlertService externAlertService; + + @BeforeEach + void setUp() { + assertEquals("alertmanager", externAlertService.supportSource()); + } + + @Test + void testAddExternAlertWithEndsAtZeroEpochTime() { + PrometheusExternAlert prometheusAlert = PrometheusExternAlert.builder() + .labels(Map.of( + "alertname", "HighCPUUsage", + "instance", "server1", + "job", "node_exporter" + )) + .annotations(Map.of( + "summary", "High CPU usage detected", + "description", "CPU usage is above 80%" + )) + .startsAt(Instant.now()) + .endsAt(Instant.EPOCH) // 0001-01-01T00:00:00Z, epoch second = 0 + .generatorURL("http://prometheus:9090/graph") + .build(); + + AlertManagerExternAlert alertManagerAlert = AlertManagerExternAlert.builder() + .groupKey("test-group-key") + .externalURL("http://alertmanager:9093") + .version("4") + .status("firing") + .groupLabels(Map.of("alertname", "HighCPUUsage")) + .commonLabels(Map.of("monitor", "prometheus")) + .commonAnnotations(Map.of("summary", "Group summary")) + .alerts(List.of(prometheusAlert)) + .build(); + + final SingleAlert[] capturedAlert = new SingleAlert[1]; + doAnswer(invocation -> { + capturedAlert[0] = invocation.getArgument(0); + return null; + }).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class)); + + externAlertService.addExternAlert(JsonUtil.toJson(alertManagerAlert)); + + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + + assertNotNull(capturedAlert[0]); + assertEquals(CommonConstants.ALERT_STATUS_FIRING, capturedAlert[0].getStatus()); + assertNull(capturedAlert[0].getEndAt(), "endAt should be null for firing alert with endsAt epoch=0"); + assertNotNull(capturedAlert[0].getActiveAt(), "activeAt should be set for firing alert"); + assertNotNull(capturedAlert[0].getStartAt(), "startAt should be set"); + assertEquals("CPU usage is above 80%", capturedAlert[0].getContent()); + + assertEquals("alertmanager", capturedAlert[0].getLabels().get("__source__")); + assertEquals("HighCPUUsage", capturedAlert[0].getLabels().get("alertname")); + assertEquals("server1", capturedAlert[0].getLabels().get("instance")); + } + + @Test + void testAddExternAlertWithEndsAtInFuture() { + PrometheusExternAlert prometheusAlert = PrometheusExternAlert.builder() + .labels(Map.of( + "alertname", "HighMemoryUsage", + "instance", "server2" + )) + .annotations(Map.of( + "summary", "High memory usage" + )) + .startsAt(Instant.now()) + .endsAt(Instant.now().plusSeconds(3600)) // 1 hour in the future + .build(); + + AlertManagerExternAlert alertManagerAlert = AlertManagerExternAlert.builder() + .groupKey("test-group-key-2") + .alerts(List.of(prometheusAlert)) + .build(); + + final SingleAlert[] capturedAlert = new SingleAlert[1]; + doAnswer(invocation -> { + capturedAlert[0] = invocation.getArgument(0); + return null; + }).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class)); + + externAlertService.addExternAlert(JsonUtil.toJson(alertManagerAlert)); + + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + + assertNotNull(capturedAlert[0]); + assertEquals(CommonConstants.ALERT_STATUS_FIRING, capturedAlert[0].getStatus()); + assertNull(capturedAlert[0].getEndAt(), "endAt should be null for firing alert"); + } + + @Test + void testAddExternAlertWithEndsAtInPast() { + PrometheusExternAlert prometheusAlert = PrometheusExternAlert.builder() + .labels(Map.of( + "alertname", "RecoveredAlert", + "instance", "server3" + )) + .annotations(Map.of( + "summary", "Alert has been resolved" + )) + .startsAt(Instant.now().minusSeconds(7200)) + .endsAt(Instant.now().minusSeconds(3600)) // 1 hour ago (resolved) + .build(); + + AlertManagerExternAlert alertManagerAlert = AlertManagerExternAlert.builder() + .groupKey("test-group-key-3") + .alerts(List.of(prometheusAlert)) + .build(); + + final SingleAlert[] capturedAlert = new SingleAlert[1]; + doAnswer(invocation -> { + capturedAlert[0] = invocation.getArgument(0); + return null; + }).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class)); + + externAlertService.addExternAlert(JsonUtil.toJson(alertManagerAlert)); + + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + + assertNotNull(capturedAlert[0]); + assertEquals(CommonConstants.ALERT_STATUS_RESOLVED, capturedAlert[0].getStatus()); + assertNotNull(capturedAlert[0].getEndAt(), "endAt should be set for resolved alert"); + assertNull(capturedAlert[0].getActiveAt(), "activeAt should be null for resolved alert"); + } + + @Test + void testAddExternAlertWithInvalidContent() { + String invalidContent = "invalid json content"; + externAlertService.addExternAlert(invalidContent); + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testAddExternAlertWithEmptyAlerts() { + AlertManagerExternAlert alertManagerAlert = AlertManagerExternAlert.builder() + .groupKey("test-group-key") + .alerts(List.of()) // Empty alerts list + .build(); + externAlertService.addExternAlert(JsonUtil.toJson(alertManagerAlert)); + verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); + } + + @Test + void testAddExternAlertWithNullLabels() { + PrometheusExternAlert prometheusAlert = PrometheusExternAlert.builder() + .labels(null) + .annotations(Map.of("summary", "Test alert")) + .startsAt(Instant.now()) + .endsAt(Instant.EPOCH) // Firing alert + .build(); + + AlertManagerExternAlert alertManagerAlert = AlertManagerExternAlert.builder() + .groupKey("test-group-key-null-labels") + .alerts(List.of(prometheusAlert)) + .build(); + + final SingleAlert[] capturedAlert = new SingleAlert[1]; + doAnswer(invocation -> { + capturedAlert[0] = invocation.getArgument(0); + return null; + }).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class)); + + externAlertService.addExternAlert(JsonUtil.toJson(alertManagerAlert)); + + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + + assertNotNull(capturedAlert[0]); + assertNotNull(capturedAlert[0].getLabels()); + assertEquals("alertmanager", capturedAlert[0].getLabels().get("__source__")); + } + + @Test + void testAddExternAlertWithNullAnnotations() { + Map<String, String> labels = new HashMap<>(); + labels.put("alertname", "TestAlert"); + + PrometheusExternAlert prometheusAlert = PrometheusExternAlert.builder() + .labels(labels) + .annotations(null) + .startsAt(Instant.now()) + .endsAt(Instant.EPOCH) + .build(); + + AlertManagerExternAlert alertManagerAlert = AlertManagerExternAlert.builder() + .groupKey("test-group-key-null-annotations") + .alerts(List.of(prometheusAlert)) + .build(); + + final SingleAlert[] capturedAlert = new SingleAlert[1]; + doAnswer(invocation -> { + capturedAlert[0] = invocation.getArgument(0); + return null; + }).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class)); + + externAlertService.addExternAlert(JsonUtil.toJson(alertManagerAlert)); + + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + + assertNotNull(capturedAlert[0]); + assertEquals("", capturedAlert[0].getContent()); + } + + @Test + void testAddExternAlertWithEndsAtNull() { + PrometheusExternAlert prometheusAlert = PrometheusExternAlert.builder() + .labels(Map.of("alertname", "NullEndsAtAlert")) + .annotations(Map.of("summary", "Test alert with null endsAt")) + .startsAt(Instant.now()) + .endsAt(null) + .build(); + + AlertManagerExternAlert alertManagerAlert = AlertManagerExternAlert.builder() + .groupKey("test-group-key-null-endsat") + .alerts(List.of(prometheusAlert)) + .build(); + + final SingleAlert[] capturedAlert = new SingleAlert[1]; + doAnswer(invocation -> { + capturedAlert[0] = invocation.getArgument(0); + return null; + }).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class)); + + externAlertService.addExternAlert(JsonUtil.toJson(alertManagerAlert)); + + verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class)); + + assertNotNull(capturedAlert[0]); + assertEquals(CommonConstants.ALERT_STATUS_FIRING, capturedAlert[0].getStatus()); + assertNull(capturedAlert[0].getEndAt(), "endAt should be null when endsAt is null"); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImplTest.java new file mode 100644 index 0000000..098560e --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImplTest.java @@ -0,0 +1,73 @@ +/* + * 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.service.impl; + +import com.google.common.collect.Lists; +import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.apache.hertzbeat.common.support.exception.SendMessageException; +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 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.Mockito.when; + +/** + * Test case for {@link SmsLocalSmsClientImpl} + */ +@ExtendWith(MockitoExtension.class) +public class SmsLocalSmsClientImplTest { + + @Mock + private SmslocalSmsProperties smslocalSmsProperties; + + private SmsLocalSmsClientImpl smsLocalSmsClient; + + + @BeforeEach + void setUp() { + smsLocalSmsClient = new SmsLocalSmsClientImpl(smslocalSmsProperties); + when(smslocalSmsProperties.getApiKey()).thenReturn("2"); + } + + @Test + void testSendMessage() { + assertEquals("smslocal", smsLocalSmsClient.getType()); + assertTrue(smsLocalSmsClient.checkConfig()); + // + NoticeReceiver noticeReceiver = new NoticeReceiver(); + noticeReceiver.setPhone("13888888888"); + + SingleAlert singleAlert = new SingleAlert(); + singleAlert.setContent("test"); + + GroupAlert groupAlert = new GroupAlert(); + groupAlert.setAlerts(Lists.newArrayList(singleAlert)); + + assertThrows(SendMessageException.class, + () -> smsLocalSmsClient.sendMessage(noticeReceiver, null, groupAlert)); + } + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/UniSmsClientImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/UniSmsClientImplTest.java new file mode 100644 index 0000000..86aff19 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/UniSmsClientImplTest.java @@ -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.alert.service.impl; + +import org.apache.hertzbeat.common.entity.dto.sms.UniSmsProperties; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; +import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; +import org.apache.hertzbeat.common.support.exception.SendMessageException; +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 java.util.HashMap; +import java.util.Map; + +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.Mockito.when; + + +/** + * Test case for {@link UniSmsClientImpl} + */ +@ExtendWith(MockitoExtension.class) +public class UniSmsClientImplTest { + + @Mock + private UniSmsProperties uniSmsProperties; + + private UniSmsClientImpl uniSmsClient; + + @BeforeEach + void setUp() { + uniSmsClient = new UniSmsClientImpl(uniSmsProperties); + when(uniSmsProperties.getSignature()).thenReturn("2"); + when(uniSmsProperties.getTemplateId()).thenReturn("any(String.class)"); + when(uniSmsProperties.getAuthMode()).thenReturn("hmac"); + when(uniSmsProperties.getAccessKeyId()).thenReturn("hmac"); + when(uniSmsProperties.getAccessKeySecret()).thenReturn("hmac"); + } + + @Test + void testSendMessage() { + assertEquals("unisms", uniSmsClient.getType()); + assertTrue(uniSmsClient.checkConfig()); + // + NoticeReceiver noticeReceiver = new NoticeReceiver(); + noticeReceiver.setPhone("13888888888"); + + Map<String, String> commonLabels = new HashMap<>(); + commonLabels.put("instance", ""); + commonLabels.put("priority", "unknown"); + + Map<String, String> commonAnnotations = new HashMap<>(); + commonAnnotations.put("test", "test"); + + GroupAlert groupAlert = new GroupAlert(); + groupAlert.setCommonLabels(commonLabels); + groupAlert.setCommonAnnotations(commonAnnotations); + + assertThrows(SendMessageException.class, + () -> uniSmsClient.sendMessage(noticeReceiver, null, groupAlert)); + + } + + +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertTemplateUtilTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertTemplateUtilTest.java new file mode 100644 index 0000000..79b782e --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertTemplateUtilTest.java @@ -0,0 +1,285 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link AlertTemplateUtil} + */ +class AlertTemplateUtilTest { + + @BeforeEach + void setUp() { + } + + @Test + void render() { + // test null template case + Map<String, Object> param = new HashMap<>(); + String template = null; + assertNull(AlertTemplateUtil.render(null, param)); + // test null map case + template = "${key} for testing"; + assertEquals(AlertTemplateUtil.render(template, null), template); + // test null template and null map case + assertNull(AlertTemplateUtil.render(null, null)); + // test illegal template case + template = ""; + param.put("key", "Just"); + assertEquals(AlertTemplateUtil.render(template, param), template); + template = "key for testing!"; + assertEquals(AlertTemplateUtil.render(template, param), template); + // test empty map case + param.clear(); + template = "${key} for testing"; + assertEquals(AlertTemplateUtil.render(template, param), "NullValue for testing"); + // test illegal template and empty map case + param.clear(); + template = "key for testing"; + assertEquals(AlertTemplateUtil.render(template, param), template); + // test one param + param.put("key", "Just"); + template = "${key} for testing"; + assertEquals(AlertTemplateUtil.render(template, param), "Just for testing"); + // test two param + param.put("key1", "Just"); + param.put("key2", "testing"); + template = "${key1} for ${key2}"; + assertEquals(AlertTemplateUtil.render(template, param), "Just for testing"); + // test all param + param.put("key1", "Just"); + param.put("key2", "for"); + param.put("key3", "testing"); + template = "${key1} ${key2} ${key3}"; + assertEquals(AlertTemplateUtil.render(template, param), "Just for testing"); + } + + @Test + void renderSpecialCharacters() { + Map<String, Object> param = new HashMap<>(); + param.put("valueWithDollar", "$100"); + param.put("valueWithBackslash", "C:\\Users"); + + String template = "The price is ${valueWithDollar} and the path is ${valueWithBackslash}"; + + // Expected to handle the dollar sign and backslash correctly without throwing an exception + String expectedResult = "The price is $100 and the path is C:\\Users"; + assertEquals(expectedResult, AlertTemplateUtil.render(template, param)); + } + + @Test + void renderNestedMapProperties() { + Map<String, Object> param = new HashMap<>(); + Map<String, Object> log = new HashMap<>(); + Map<String, Object> attributes = new HashMap<>(); + + attributes.put("level", "ERROR"); + attributes.put("thread", "main"); + log.put("attributes", attributes); + log.put("message", "Connection failed"); + log.put("timestamp", "2024-01-01T10:00:00Z"); + + param.put("log", log); + param.put("instance", "server-01"); + + String template = "Log alert: ${log.attributes.level} - ${log.message} on ${instance}"; + String expectedResult = "Log alert: ERROR - Connection failed on server-01"; + assertEquals(expectedResult, AlertTemplateUtil.render(template, param)); + } + + @Test + void renderMultiLevelNestedProperties() { + Map<String, Object> param = new HashMap<>(); + Map<String, Object> config = new HashMap<>(); + Map<String, Object> database = new HashMap<>(); + Map<String, Object> connection = new HashMap<>(); + + connection.put("host", "localhost"); + connection.put("port", 3306); + database.put("connection", connection); + database.put("name", "hertzbeat"); + config.put("database", database); + + param.put("config", config); + + String template = "Database: ${config.database.name} at ${config.database.connection.host}:${config.database.connection.port}"; + String expectedResult = "Database: hertzbeat at localhost:3306"; + assertEquals(expectedResult, AlertTemplateUtil.render(template, param)); + } + + @Test + void renderNonExistentNestedProperties() { + Map<String, Object> param = new HashMap<>(); + Map<String, Object> log = new HashMap<>(); + + log.put("message", "Test message"); + param.put("log", log); + + // Test non-existent nested property + String template = "Log alert: ${log.nonexistent.property} - ${log.message}"; + String expectedResult = "Log alert: NullValue - Test message"; + assertEquals(expectedResult, AlertTemplateUtil.render(template, param)); + } + + @Test + void renderNonExistentTopLevelProperty() { + Map<String, Object> param = new HashMap<>(); + param.put("existing", "value"); + + String template = "Value: ${nonexistent.property}"; + String expectedResult = "Value: NullValue"; + assertEquals(expectedResult, AlertTemplateUtil.render(template, param)); + } + + @Test + void renderNestedPropertiesWithNullValues() { + Map<String, Object> param = new HashMap<>(); + Map<String, Object> data = new HashMap<>(); + + data.put("value", null); + data.put("description", "Test data"); + param.put("data", data); + + String template = "Data: ${data.value} - ${data.description}"; + String expectedResult = "Data: NullValue - Test data"; + assertEquals(expectedResult, AlertTemplateUtil.render(template, param)); + } + + @Test + void renderComplexNestedStructure() { + Map<String, Object> param = new HashMap<>(); + Map<String, Object> alert = new HashMap<>(); + Map<String, Object> monitor = new HashMap<>(); + Map<String, Object> target = new HashMap<>(); + Map<String, Object> metadata = new HashMap<>(); + + metadata.put("version", "1.0"); + metadata.put("author", "system"); + target.put("host", "192.168.1.100"); + target.put("port", 8080); + target.put("metadata", metadata); + monitor.put("name", "HTTP Monitor"); + monitor.put("target", target); + alert.put("monitor", monitor); + alert.put("severity", "CRITICAL"); + + param.put("alert", alert); + + String template = "Alert: ${alert.severity} from ${alert.monitor.name} targeting ${alert.monitor.target.host}:${alert.monitor.target.port} (v${alert.monitor.target.metadata.version})"; + String expectedResult = "Alert: CRITICAL from HTTP Monitor targeting 192.168.1.100:8080 (v1.0)"; + assertEquals(expectedResult, AlertTemplateUtil.render(template, param)); + } + + @Test + void renderWithDifferentDataTypes() { + Map<String, Object> param = new HashMap<>(); + Map<String, Object> metrics = new HashMap<>(); + + metrics.put("cpu", 85.5); + metrics.put("memory", 1024); + metrics.put("active", true); + metrics.put("count", 42L); + + param.put("metrics", metrics); + + String template = "CPU: ${metrics.cpu}%, Memory: ${metrics.memory}MB, Active: ${metrics.active}, Count: ${metrics.count}"; + String expectedResult = "CPU: 85.5%, Memory: 1024MB, Active: true, Count: 42"; + assertEquals(expectedResult, AlertTemplateUtil.render(template, param)); + } + + // Helper classes for testing object property access + public static class TestObject { + private String name; + private int value; + private TestNestedObject nested; + + public TestObject(String name, int value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public int getValue() { + return value; + } + + public TestNestedObject getNested() { + return nested; + } + + public void setNested(TestNestedObject nested) { + this.nested = nested; + } + } + + public static class TestNestedObject { + private String description; + private boolean enabled; + + public TestNestedObject(String description, boolean enabled) { + this.description = description; + this.enabled = enabled; + } + + public String getDescription() { + return description; + } + + public boolean isEnabled() { + return enabled; + } + } + + @Test + void renderWithObjectProperties() { + Map<String, Object> param = new HashMap<>(); + TestNestedObject nested = new TestNestedObject("Test description", true); + TestObject obj = new TestObject("TestName", 123); + obj.setNested(nested); + + param.put("object", obj); + + String template = "Object: ${object.name} (value: ${object.value}) - ${object.nested.description}, enabled: ${object.nested.enabled}"; + String expectedResult = "Object: TestName (value: 123) - Test description, enabled: true"; + assertEquals(expectedResult, AlertTemplateUtil.render(template, param)); + } + + @Test + void renderWithMixedMapAndObjectProperties() { + Map<String, Object> param = new HashMap<>(); + Map<String, Object> config = new HashMap<>(); + TestObject obj = new TestObject("ConfigObject", 456); + + config.put("object", obj); + config.put("type", "mixed"); + param.put("config", config); + + String template = "Config type: ${config.type}, object name: ${config.object.name}, value: ${config.object.value}"; + String expectedResult = "Config type: mixed, object name: ConfigObject, value: 456"; + assertEquals(expectedResult, AlertTemplateUtil.render(template, param)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/CryptoUtilsTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/CryptoUtilsTest.java new file mode 100644 index 0000000..99555c0 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/CryptoUtilsTest.java @@ -0,0 +1,48 @@ +/* + * 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.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test case for {@link CryptoUtils} + */ +public class CryptoUtilsTest { + + + @Test + void testSha256Hex() { + String sign = CryptoUtils.sha256Hex("Hello world."); + assertEquals("aa3ec16e6acc809d8b2818662276256abfd2f1b441cb51574933f3d4bd115d11", sign); + } + + @Test + void testHmacSha256Base64Debug() { + String signature = CryptoUtils.hmacSha256Base64("your-real-key", "your-real-data"); + assertEquals("8JrfX0v5Tt3s8PfI85o6jcf5XM3C+vLlMwvFp45LupU=", signature); + } + + @Test + void testHmacSha256Hex() { + String signature = CryptoUtils.hmacSha256Hex("your-real-key", "your-real-data");; + assertEquals("f09adf5f4bf94eddecf0f7c8f39a3a8dc7f95ccdc2faf2e5330bc5a78e4bba95", signature); + } + +} \ No newline at end of file diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/DateUtilTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/DateUtilTest.java new file mode 100644 index 0000000..e60515a --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/DateUtilTest.java @@ -0,0 +1,71 @@ +/* + * 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 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.assertTrue; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link DateUtil} + */ +class DateUtilTest { + + @Test + void getTimeStampFromSomeFormats() { + String date = "2024-05-13"; + Optional<Long> actualTimestamp = DateUtil.getTimeStampFromSomeFormats(date); + assertFalse(actualTimestamp.isPresent()); + + date = "2024-05-13T12:34:56.789Z"; + actualTimestamp = DateUtil.getTimeStampFromSomeFormats(date); + assertTrue(actualTimestamp.isPresent()); + assertEquals(1715603696789L, actualTimestamp.get()); + + date = "2023-02-22T07:27:15.404000000Z"; + actualTimestamp = DateUtil.getTimeStampFromSomeFormats(date); + assertTrue(actualTimestamp.isPresent()); + assertEquals(1677050835404L, actualTimestamp.get()); + } + + @Test + void getTimeStampFromFormat() { + String date = "2024-05-13 10:30:00"; + String format = "yyyy-MM-dd HH:mm:ss"; + Optional<Long> actualTimestamp = DateUtil.getTimeStampFromFormat(date, format); + assertTrue(actualTimestamp.isPresent()); + assertEquals(1715596200000L, actualTimestamp.get()); + + date = "2024-05-13"; + format = "yyyy-MM-dd HH:mm:ss.SSS"; + actualTimestamp = DateUtil.getTimeStampFromFormat(date, format); + assertFalse(actualTimestamp.isPresent()); + } + + @Test + void getZonedTimeStampFromFormat() { + String dataStr = "2025/06/02 22:56:15 GMT+08:00"; + Long time = DateUtil.getZonedTimeStampFromFormat(dataStr, "yyyy/MM/dd HH:mm:ss 'GMT'XXX"); + assertNotNull(time); + assertEquals(1748876175000L, time); + } + +} diff --git a/hertzbeat-base/pom.xml b/hertzbeat-base/pom.xml new file mode 100644 index 0000000..0b8a384 --- /dev/null +++ b/hertzbeat-base/pom.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-base</artifactId> + <name>${project.artifactId}</name> + <properties> + + </properties> + + <dependencies> + <!-- common --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-common-spring</artifactId> + <exclusions> + <exclusion> + <artifactId>commons-logging</artifactId> + <groupId>commons-logging</groupId> + </exclusion> + </exclusions> + </dependency> + </dependencies> +</project> diff --git a/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/dao/GeneralConfigDao.java b/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/dao/GeneralConfigDao.java new file mode 100644 index 0000000..389cc8c --- /dev/null +++ b/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/dao/GeneralConfigDao.java @@ -0,0 +1,39 @@ +/* + * 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.base.dao; + +import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Component; + +/** + * Public server configuration Dao + * todo common config data cache + * <p>This interface inherits the two interfaces JpaRepository and JpaSpecificationExecutor, providing basic CRUD operations and specification query capabilities.</p> + */ +@Component +public interface GeneralConfigDao extends JpaRepository<GeneralConfig, Long>, JpaSpecificationExecutor<GeneralConfig> { + + /** + * Query by type + * @param type type + * @return Return the queried configuration information + */ + GeneralConfig findByType(String type); +} diff --git a/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/dao/LabelDao.java b/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/dao/LabelDao.java new file mode 100644 index 0000000..372e75a --- /dev/null +++ b/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/dao/LabelDao.java @@ -0,0 +1,45 @@ +/* + * 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.base.dao; + +import java.util.Optional; +import java.util.Set; +import org.apache.hertzbeat.common.entity.manager.Label; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * Label repository + */ +public interface LabelDao extends JpaRepository<Label, Long>, JpaSpecificationExecutor<Label> { + + /** + * delete Label by Label id + * @param ids id list + */ + void deleteLabelsByIdIn(Set<Long> ids); + + /** + * find Label by name and value + * @param name Label name + * @param value Label value + * @return Label + */ + Optional<Label> findLabelByNameAndTagValue(String name, String value); + +} diff --git a/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/service/GeneralConfigService.java b/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/service/GeneralConfigService.java new file mode 100644 index 0000000..06c01cc --- /dev/null +++ b/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/service/GeneralConfigService.java @@ -0,0 +1,50 @@ +/* + * 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.base.service; + +/** + * <p>GeneralConfigService interface provides CRUD operations for configurations.</p> + * @param <T> configuration type. + * @version 1.0 + */ +public interface GeneralConfigService<T> { + + /** + * config type: email, sms + * @return type string + */ + String type(); + + /** + * save config + * @param config need save configuration + */ + void saveConfig(T config); + + /** + * get config + * @return query The configuration is queried + */ + T getConfig(); + + /** + * handler after save config + * @param config config + */ + default void handler(T config) {} +} diff --git a/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/service/LabelService.java b/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/service/LabelService.java new file mode 100644 index 0000000..5de8b7e --- /dev/null +++ b/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/service/LabelService.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.base.service; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.hertzbeat.common.entity.manager.Label; +import org.springframework.data.domain.Page; + +/** + * Label service + */ +public interface LabelService { + + /** + * new Label + * @param label label + */ + void addLabel(Label label); + + /** + * update label + * @param label label + */ + void modifyLabel(Label label); + + /** + * get label page list + * @param search label content search + * @param type label type + * @param pageIndex List current page + * @param pageSize Number of list pagination + * @return label + */ + Page<Label> getLabels(String search, Byte type, int pageIndex, int pageSize); + + /** + * delete labels + * @param ids label id list + */ + void deleteLabels(HashSet<Long> ids); + + /** + * Identifies new labels from the given entry that are not yet in the database + * @param originLabels k-v entry + * @return new labels + */ + List<Label> determineNewLabels(Set<Map.Entry<String, String>> originLabels); +} diff --git a/hertzbeat-collector/.mvn/wrapper/maven-wrapper.properties b/hertzbeat-collector/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /dev/null +++ b/hertzbeat-collector/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# 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. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/hertzbeat-collector/hertzbeat-collector-basic/pom.xml b/hertzbeat-collector/hertzbeat-collector-basic/pom.xml new file mode 100644 index 0000000..63c88e2 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/pom.xml @@ -0,0 +1,186 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + + <artifactId>hertzbeat-collector-basic</artifactId> + <name>${project.artifactId}</name> + + <properties> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <mqtt.version>1.2.5</mqtt.version> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-common</artifactId> + </dependency> + <!-- spring --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + <scope>provided</scope> + </dependency> + <!-- run sql script--> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-jdbc</artifactId> + </dependency> + <!-- kafka --> + <dependency> + <groupId>org.apache.kafka</groupId> + <artifactId>kafka-clients</artifactId> + </dependency> + <!-- http --> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + </dependency> + <!--network--> + <dependency> + <groupId>commons-net</groupId> + <artifactId>commons-net</artifactId> + </dependency> + <!-- clickhouse --> + <dependency> + <groupId>com.clickhouse</groupId> + <artifactId>clickhouse-jdbc</artifactId> + </dependency> + <!-- dm --> + <dependency> + <groupId>com.dameng</groupId> + <artifactId>DmJdbcDriver18</artifactId> + </dependency> + <dependency> + <groupId>com.xugudb</groupId> + <artifactId>xugu-jdbc</artifactId> + </dependency> + <!-- postgresql --> + <dependency> + <groupId>org.postgresql</groupId> + <artifactId>postgresql</artifactId> + </dependency> + <dependency> + <groupId>net.i2p.crypto</groupId> + <artifactId>eddsa</artifactId> + <version>${eddsa.version}</version> + </dependency> + <!-- sql server --> + <dependency> + <groupId>com.microsoft.sqlserver</groupId> + <artifactId>mssql-jdbc</artifactId> + </dependency> + <!-- oracle --> + <dependency> + <groupId>com.oracle.database.jdbc</groupId> + <artifactId>ojdbc8</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.oracle.database.nls</groupId> + <artifactId>orai18n</artifactId> + <scope>provided</scope> + </dependency> + <!-- db2 --> + <dependency> + <groupId>com.ibm.db2</groupId> + <artifactId>jcc</artifactId> + <scope>provided</scope> + </dependency> + <!--redis--> + <dependency> + <groupId>io.lettuce</groupId> + <artifactId>lettuce-core</artifactId> + </dependency> + <!--snmp--> + <dependency> + <groupId>org.snmp4j</groupId> + <artifactId>snmp4j</artifactId> + <version>${snmp4j.version}</version> + </dependency> + <!--dns--> + <dependency> + <groupId>dnsjava</groupId> + <artifactId>dnsjava</artifactId> + <version>${dnsjava.version}</version> + </dependency> + <!-- consul --> + <dependency> + <groupId>com.ecwid.consul</groupId> + <artifactId>consul-api</artifactId> + <version>${consul-api.version}</version> + </dependency> + <!-- nacos --> + <dependency> + <groupId>com.alibaba.nacos</groupId> + <artifactId>nacos-client</artifactId> + <version>${nacos-client.version}</version> + </dependency> + <!-- mqtt --> + <dependency> + <groupId>org.eclipse.paho</groupId> + <artifactId>org.eclipse.paho.client.mqttv3</artifactId> + <version>${mqtt.version}</version> + </dependency> + <!--Bouncy Castle--> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcpkix-jdk15on</artifactId> + <version>1.68</version> + </dependency> + + + <!--plc--> + <dependency> + <groupId>org.apache.plc4x</groupId> + <artifactId>plc4j-api</artifactId> + <version>0.12.0</version> + </dependency> + <dependency> + <groupId>org.apache.plc4x</groupId> + <artifactId>plc4j-driver-modbus</artifactId> + <version>0.12.0</version> + </dependency> + <dependency> + <groupId>org.apache.plc4x</groupId> + <artifactId>plc4j-driver-s7</artifactId> + <version>0.12.0</version> + </dependency> + <dependency> + <groupId>org.apache.sshd</groupId> + <artifactId>sshd-sftp</artifactId> + </dependency> + <!-- Zookeeper --> + <dependency> + <groupId>org.apache.zookeeper</groupId> + <artifactId>zookeeper</artifactId> + <version>${zookeeper.version}</version> + </dependency> + </dependencies> +</project> diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/JdbcCommonCollect.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/JdbcCommonCollect.java new file mode 100644 index 0000000..7be1a44 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/JdbcCommonCollect.java @@ -0,0 +1,680 @@ +/* + * 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.collector.collect.database; + +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection; +import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier; +import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache; +import org.apache.hertzbeat.collector.collect.common.cache.JdbcConnect; +import org.apache.hertzbeat.collector.collect.database.query.JdbcQueryExecutor; +import org.apache.hertzbeat.collector.collect.database.query.JdbcQueryExecutorRegistry; +import org.apache.hertzbeat.collector.collect.database.query.JdbcQueryRowSet; +import org.apache.hertzbeat.collector.collect.common.ssh.SshTunnelHelper; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.SshTunnel; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.channel.exception.SshChannelOpenException; +import org.postgresql.util.PSQLException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.util.StringUtils; + +/** + * common query for database query + */ +@Slf4j +public class JdbcCommonCollect extends AbstractCollect { + + private static final String QUERY_TYPE_ONE_ROW = "oneRow"; + private static final String QUERY_TYPE_MULTI_ROW = "multiRow"; + private static final String QUERY_TYPE_COLUMNS = "columns"; + private static final String RUN_SCRIPT = "runScript"; + private static final int CONNECTION_LOCK_STRIPES = 64; + private static final Object[] CONNECTION_LOCKS = createConnectionLocks(); + + private static final String[] VULNERABLE_KEYWORDS = {"allowLoadLocalInfile", "allowLoadLocalInfileInPath", "useLocalInfile"}; + + private static final String[] BLACK_LIST = { + // dangerous SQL commands - may cause database structure damage or data leakage + "create trigger", "create alias", "runscript from", "shutdown", "drop table", + "drop database", "create function", "alter system", "grant all", "revoke all", + + // file IO related - may cause server files to be read or written + "allowloadlocalinfile", "allowloadlocalinfileinpath", "uselocalinfile", + + // code execution related - may result in remote code execution + "init=", "javaobjectserializer=", "runscript", "serverstatusdiffinterceptor", + "queryinterceptors=", "statementinterceptors=", "exceptioninterceptors=", + "xp_cmdshell", "create function", "dbms_java", "sp_sysexecute", "load_file", + + // multiple statement execution - may lead to SQL injection + "allowmultiqueries", + + // deserialization related - may result in remote code execution + "autodeserialize", "detectcustomcollations", + }; + + // universal bypass detection mode - applicable to all databases for dangerous command bypass detection + private static final String[] UNIVERSAL_BYPASS_PATTERNS = { + ".*create\\s*([/\\\\]|\\\\n|/n|\\n)\\s*trigger.*", + ".*create\\s*([/\\\\]|\\\\n|/n|\\n)\\s*function.*", + ".*drop\\s*([/\\\\]|\\\\n|/n|\\n)\\s*table.*", + ".*drop\\s*([/\\\\]|\\\\n|/n|\\n)\\s*database.*", + ".*run\\s*([/\\\\]|\\\\n|/n|\\n)\\s*script.*", + ".*alter\\s*([/\\\\]|\\\\n|/n|\\n)\\s*system.*", + ".*grant\\s*([/\\\\]|\\\\n|/n|\\n)\\s*all.*", + ".*revoke\\s*([/\\\\]|\\\\n|/n|\\n)\\s*all.*", + ".*xp\\s*([/\\\\]|\\\\n|/n|\\n)\\s*cmdshell.*", + ".*load\\s*([/\\\\]|\\\\n|/n|\\n)\\s*file.*" + }; + + // database platform specific bypass detection mode + private static final HashMap<String, String[]> PLATFORM_BYPASS_PATTERNS = new HashMap<>(); + + static { + // H2 database special character bypass mode + PLATFORM_BYPASS_PATTERNS.put("h2", new String[]{ + ".*(\\\\\\\\|/|\\\\|\\\\n|/n|\\n)\\s*init\\s*=.*", + ".*in\\s*([/\\\\]|\\\\n|/n|\\n)\\s*it\\s*=.*", + ".*(\\\\\\\\|/|\\\\|\\\\n|/n|\\n)\\s*runscript\\s+from.*", + ".*ru\\s*([/\\\\]|\\\\n|/n|\\n)\\s*script\\s+from.*" + }); + + // MySQL/MariaDB bypass mode + String[] mysqlPatterns = { + ".*allow\\s*([/\\\\]|\\\\n|/n|\\n)\\s*load\\s*([/\\\\]|\\\\n|/n|\\n)\\s*local\\s*([/\\\\]|\\\\n|/n|\\n)\\s*infile.*", + ".*allow\\s*([/\\\\]|\\\\n|/n|\\n)\\s*multi\\s*([/\\\\]|\\\\n|/n|\\n)\\s*queries.*", + ".*query\\s*([/\\\\]|\\\\n|/n|\\n)\\s*interceptors.*", + ".*statement\\s*([/\\\\]|\\\\n|/n|\\n)\\s*interceptors.*", + ".*exception\\s*([/\\\\]|\\\\n|/n|\\n)\\s*interceptors.*", + ".*auto\\s*([/\\\\]|\\\\n|/n|\\n)\\s*deserialize.*" + }; + PLATFORM_BYPASS_PATTERNS.put("mysql", mysqlPatterns); + PLATFORM_BYPASS_PATTERNS.put("mariadb", mysqlPatterns); + + // PostgreSQL bypass mode + PLATFORM_BYPASS_PATTERNS.put("postgresql", new String[]{ + ".*socket\\s*([/\\\\]|\\\\n|/n|\\n)\\s*factory.*", + ".*logger\\s*([/\\\\]|\\\\n|/n|\\n)\\s*file.*", + ".*ssl\\s*([/\\\\]|\\\\n|/n|\\n)\\s*mode.*", + ".*logger\\s*([/\\\\]|\\\\n|/n|\\n)\\s*level.*" + }); + + // SQL Server bypass mode + PLATFORM_BYPASS_PATTERNS.put("sqlserver", new String[]{ + ".*integrated\\s*([/\\\\]|\\\\n|/n|\\n)\\s*security.*", + ".*authentication\\s*([/\\\\]|\\\\n|/n|\\n)\\s*scheme.*", + ".*select\\s*([/\\\\]|\\\\n|/n|\\n)\\s*method.*", + ".*send\\s*([/\\\\]|\\\\n|/n|\\n)\\s*string\\s*([/\\\\]|\\\\n|/n|\\n)\\s*parameters\\s*([/\\\\]|\\\\n|/n|\\n)\\s*as\\s*([/\\\\]|\\\\n|/n|\\n)\\s*unicode.*", + ".*x\\s*([/\\\\]|\\\\n|/n|\\n)\\s*open\\s*([/\\\\]|\\\\n|/n|\\n)\\s*state.*", + ".*application\\s*([/\\\\]|\\\\n|/n|\\n)\\s*intent.*" + }); + + // ClickHouse bypass mode + PLATFORM_BYPASS_PATTERNS.put("clickhouse", new String[]{ + ".*custom\\s*([/\\\\]|\\\\n|/n|\\n)\\s*http\\s*([/\\\\]|\\\\n|/n|\\n)\\s*params.*", + ".*http\\s*([/\\\\]|\\\\n|/n|\\n)\\s*connection\\s*([/\\\\]|\\\\n|/n|\\n)\\s*provider.*", + ".*check\\s*([/\\\\]|\\\\n|/n|\\n)\\s*all\\s*([/\\\\]|\\\\n|/n|\\n)\\s*nodes.*", + ".*fail\\s*([/\\\\]|\\\\n|/n|\\n)\\s*over.*", + ".*use\\s*([/\\\\]|\\\\n|/n|\\n)\\s*objects\\s*([/\\\\]|\\\\n|/n|\\n)\\s*in\\s*([/\\\\]|\\\\n|/n|\\n)\\s*arrays.*" + }); + + // Oracle bypass mode + PLATFORM_BYPASS_PATTERNS.put("oracle", new String[]{ + ".*oracle\\s*([/\\\\]|\\\\n|/n|\\n)\\s*jdbc.*", + ".*oracle\\s*([/\\\\]|\\\\n|/n|\\n)\\s*net.*", + ".*oracle\\.jdbc\\.timezoneinfotable\\s*=.*", + ".*oracle\\.net\\.wallet_location\\s*=.*", + ".*oracle\\.net\\.ssl_server_dn_match\\s*=\\s*false.*", + ".*oracle\\.jdbc\\.enablesqlinjectionattack\\s*=\\s*true.*", + ".*oracle\\.jdbc\\.implicitstatementcachesize\\s*=\\s*0.*", + ".*oracle\\.jdbc\\.timezoneinfotable\\s*=.*", + ".*oracle\\.net\\.wallet_location\\s*=.*", + ".*oracle\\.net\\.ssl_server_dn_match\\s*=\\s*false.*", + ".*oracle\\.jdbc\\.enablesqlinjectionattack\\s*=\\s*true.*", + ".*oracle\\.jdbc\\.implicitstatementcachesize\\s*=\\s*0.*" + }); + + // DM bypass mode + PLATFORM_BYPASS_PATTERNS.put("dm", new String[]{ + ".*login\\s*([/\\\\]|\\\\n|/n|\\n)\\s*mode.*", + ".*compatible\\s*([/\\\\]|\\\\n|/n|\\n)\\s*mode.*", + ".*en\\s*([/\\\\]|\\\\n|/n|\\n)\\s*crypt.*", + ".*ci\\s*([/\\\\]|\\\\n|/n|\\n)\\s*pher.*" + }); + + // DB2 bypass mode + PLATFORM_BYPASS_PATTERNS.put("db2", new String[]{ + ".*security\\s*([/\\\\]|\\\\n|/n|\\n)\\s*mechanism.*", + ".*client\\s*([/\\\\]|\\\\n|/n|\\n)\\s*accounting.*", + ".*ssl\\s*([/\\\\]|\\\\n|/n|\\n)\\s*connection.*", + ".*use\\s*([/\\\\]|\\\\n|/n|\\n)\\s*client\\s*([/\\\\]|\\\\n|/n|\\n)\\s*information.*" + }); + } + + private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance(); + + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getJdbc() == null) { + throw new IllegalArgumentException("Database collect must has jdbc params"); + } + if (StringUtils.hasText(metrics.getJdbc().getUrl())) { + String url = metrics.getJdbc().getUrl().toLowerCase(); + for (String keyword : VULNERABLE_KEYWORDS) { + if (url.contains(keyword.toLowerCase())) { + throw new IllegalArgumentException("Jdbc url prohibit contains vulnerable param " + keyword); + } + } + } + SshTunnelHelper.checkTunnelParam(metrics.getJdbc().getSshTunnel()); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + JdbcProtocol jdbcProtocol = metrics.getJdbc(); + int timeout = CollectUtil.getTimeout(jdbcProtocol.getTimeout()); + boolean reuseConnection = Boolean.parseBoolean(jdbcProtocol.getReuseConnection()); + try { + switch (jdbcProtocol.getQueryType()) { + case QUERY_TYPE_ONE_ROW -> { + try (JdbcQueryRowSet rowSet = executeQuery(metrics, timeout, reuseConnection, 1)) { + queryOneRow(rowSet, metrics.getAliasFields(), builder, startTime); + } + } + case QUERY_TYPE_MULTI_ROW -> { + try (JdbcQueryRowSet rowSet = executeQuery(metrics, timeout, reuseConnection, 1000)) { + queryMultiRow(rowSet, metrics.getAliasFields(), builder, startTime); + } + } + case QUERY_TYPE_COLUMNS -> { + try (JdbcQueryRowSet rowSet = executeQuery(metrics, timeout, reuseConnection, 1000)) { + queryOneRowByMatchTwoColumns(rowSet, metrics.getAliasFields(), builder, startTime); + } + } + case RUN_SCRIPT -> runScript(metrics, timeout, reuseConnection); + default -> { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Not support database query type: " + jdbcProtocol.getQueryType()); + } + } + } catch (PSQLException psqlException) { + // for PostgreSQL 08001 + if (CollectorConstants.POSTGRESQL_UN_REACHABLE_CODE.equals(psqlException.getSQLState())) { + // Peer connection failed, unreachable + builder.setCode(CollectRep.Code.UN_REACHABLE); + } else { + builder.setCode(CollectRep.Code.FAIL); + } + builder.setMsg("Error: " + psqlException.getMessage() + " Code: " + psqlException.getSQLState()); + } catch (SQLException sqlException) { + log.warn("Jdbc sql error: {}, code: {}.", sqlException.getMessage(), sqlException.getErrorCode()); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Query Error: " + sqlException.getMessage() + " Code: " + sqlException.getErrorCode()); + } catch (SshException sshException) { + Throwable throwable = sshException.getCause(); + if (throwable instanceof SshChannelOpenException) { + log.warn("[Jdbc collect] Remote ssh server no more session channel, please increase sshd_config MaxSessions."); + } + String errorMsg = CommonUtil.getMessageFromThrowable(sshException); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer ssh connection failed: " + errorMsg); + } catch (Exception e) { + String errorMessage = CommonUtil.getMessageFromThrowable(e); + log.error("Jdbc error: {}.", errorMessage, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Query Error: " + errorMessage); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_JDBC; + } + + private JdbcQueryRowSet executeQuery(Metrics metrics, int timeout, boolean reuseConnection, int maxRows) throws Exception { + Optional<JdbcQueryExecutor> executor = JdbcQueryExecutorRegistry.resolve(metrics); + if (executor.isPresent()) { + return executor.get().executeQuery(metrics, timeout, maxRows); + } + return executeJdbcQuery(metrics.getJdbc(), timeout, reuseConnection, maxRows); + } + + private JdbcQueryRowSet executeJdbcQuery(JdbcProtocol jdbcProtocol, int timeout, boolean reuseConnection, + int maxRows) throws Exception { + Statement statement = null; + try { + String databaseUrl = resolveDatabaseUrl(jdbcProtocol); + statement = getConnection(jdbcProtocol.getUsername(), + jdbcProtocol.getPassword(), databaseUrl, timeout, reuseConnection); + statement.setMaxRows(maxRows); + return new ResultSetJdbcQueryRowSet(statement, statement.executeQuery(jdbcProtocol.getSql()), reuseConnection); + } catch (Exception exception) { + closeStatementAndConnection(statement, reuseConnection); + throw exception; + } + } + + private void runScript(Metrics metrics, int timeout, boolean reuseConnection) throws Exception { + JdbcProtocol jdbcProtocol = metrics.getJdbc(); + Statement statement = null; + try { + String databaseUrl = resolveDatabaseUrl(jdbcProtocol); + statement = getConnection(jdbcProtocol.getUsername(), + jdbcProtocol.getPassword(), databaseUrl, timeout, reuseConnection); + Connection connection = statement.getConnection(); + FileSystemResource rc = new FileSystemResource(jdbcProtocol.getSql()); + ScriptUtils.executeSqlScript(connection, rc); + } finally { + closeStatementAndConnection(statement, reuseConnection); + } + } + + private String resolveDatabaseUrl(JdbcProtocol jdbcProtocol) throws Exception { + SshTunnel sshTunnel = jdbcProtocol.getSshTunnel(); + if (sshTunnel != null && Boolean.parseBoolean(sshTunnel.getEnable())) { + int localPort = SshTunnelHelper.localPortForward(sshTunnel, jdbcProtocol.getHost(), jdbcProtocol.getPort()); + return constructDatabaseUrl(jdbcProtocol, "localhost", String.valueOf(localPort)); + } + return constructDatabaseUrl(jdbcProtocol, jdbcProtocol.getHost(), jdbcProtocol.getPort()); + } + + private Statement getConnection(String username, String password, String url, Integer timeout, boolean reuseConnection) throws Exception { + CacheIdentifier identifier = CacheIdentifier.builder() + .ip(url) + .username(username).password(password).build(); + if (!reuseConnection) { + return getConnection(username, password, url, timeout, false, identifier); + } + Object lock = CONNECTION_LOCKS[Math.floorMod(identifier.hashCode(), CONNECTION_LOCKS.length)]; + synchronized (lock) { + return getConnection(username, password, url, timeout, true, identifier); + } + } + + private Statement getConnection(String username, String password, String url, Integer timeout, + boolean reuseConnection, CacheIdentifier identifier) throws Exception { + Statement statement = null; + if (reuseConnection) { + Optional<AbstractConnection<?>> cacheOption = connectionCommonCache.getCache(identifier, true); + if (cacheOption.isPresent()) { + JdbcConnect jdbcConnect = (JdbcConnect) cacheOption.get(); + try { + statement = jdbcConnect.getConnection().createStatement(); + // set query timeout + int timeoutSecond = timeout / 1000; + timeoutSecond = timeoutSecond <= 0 ? 1 : timeoutSecond; + statement.setQueryTimeout(timeoutSecond); + // set query max row number + statement.setMaxRows(1000); + } catch (Exception e) { + log.info("The jdbc connect from cache, create statement error: {}", e.getMessage()); + try { + if (statement != null) { + statement.close(); + } + jdbcConnect.close(); + } catch (Exception e2) { + log.error(e2.getMessage()); + } + statement = null; + connectionCommonCache.removeCache(identifier); + } + } + if (statement != null) { + return statement; + } + } + Connection connection = null; + try { + // renew connection when failed + connection = openConnection(url, username, password); + connection.setReadOnly(true); + statement = connection.createStatement(); + int timeoutSecond = timeout / 1000; + timeoutSecond = timeoutSecond <= 0 ? 1 : timeoutSecond; + statement.setQueryTimeout(timeoutSecond); + statement.setMaxRows(1000); + if (reuseConnection) { + JdbcConnect jdbcConnect = new JdbcConnect(connection); + connectionCommonCache.addCache(identifier, jdbcConnect); + } + } catch (Exception exception) { + try { + if (statement != null) { + statement.close(); + } + } catch (Exception closeException) { + log.error("Jdbc close statement error: {}", closeException.getMessage()); + } + try { + if (connection != null) { + connection.close(); + } + } catch (Exception closeException) { + log.error("Jdbc close connection error: {}", closeException.getMessage()); + } + throw exception; + } + return statement; + } + + private static Object[] createConnectionLocks() { + Object[] locks = new Object[CONNECTION_LOCK_STRIPES]; + for (int i = 0; i < locks.length; i++) { + locks[i] = new Object(); + } + return locks; + } + + Connection openConnection(String url, String username, String password) throws SQLException { + return DriverManager.getConnection(url, username, password); + } + + /** + * query one row record, response metrics header and one value row + * eg: + * query metrics:one tow three four + * query sql:select one, tow, three, four from book limit 1; + * + * @param rowSet row set + * @param columns query metrics field list + * @throws Exception when error happen + */ + private void queryOneRow(JdbcQueryRowSet rowSet, List<String> columns, + CollectRep.MetricsData.Builder builder, long startTime) throws Exception { + if (rowSet.next()) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String column : columns) { + if (CollectorConstants.RESPONSE_TIME.equals(column)) { + long time = System.currentTimeMillis() - startTime; + valueRowBuilder.addColumn(String.valueOf(time)); + } else { + String value = rowSet.getString(column); + value = value == null ? CommonConstants.NULL_VALUE : value; + valueRowBuilder.addColumn(value); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + + /** + * query two columns to mapping one row + * eg: + * query metrics:one two three four + * query sql:select key, value from book; the key is the query metrics fields + * select key, value from book; + * one - value1 + * two - value2 + * three - value3 + * four - value4 + * + * @param rowSet row set + * @param columns query metrics field list + * @throws Exception when error happen + */ + private void queryOneRowByMatchTwoColumns(JdbcQueryRowSet rowSet, List<String> columns, + CollectRep.MetricsData.Builder builder, long startTime) throws Exception { + HashMap<String, String> values = new HashMap<>(columns.size()); + while (rowSet.next()) { + if (rowSet.getString(1) != null) { + values.put(rowSet.getString(1).toLowerCase().trim(), rowSet.getString(2)); + } + } + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String column : columns) { + if (CollectorConstants.RESPONSE_TIME.equals(column)) { + long time = System.currentTimeMillis() - startTime; + valueRowBuilder.addColumn(String.valueOf(time)); + } else { + String value = values.get(column.toLowerCase()); + value = value == null ? CommonConstants.NULL_VALUE : value; + valueRowBuilder.addColumn(value); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + + /** + * query multi row record, response metrics header and multi value row + * eg: + * query metrics:one tow three four + * query sql:select one, tow, three, four from book; + * and return multi row record mapping with the metrics + * + * @param rowSet row set + * @param columns query metrics field list + * @throws Exception when error happen + */ + private void queryMultiRow(JdbcQueryRowSet rowSet, List<String> columns, + CollectRep.MetricsData.Builder builder, long startTime) throws Exception { + while (rowSet.next()) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String column : columns) { + if (CollectorConstants.RESPONSE_TIME.equals(column)) { + long time = System.currentTimeMillis() - startTime; + valueRowBuilder.addColumn(String.valueOf(time)); + } else { + String value = rowSet.getString(column); + value = value == null ? CommonConstants.NULL_VALUE : value; + valueRowBuilder.addColumn(value); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + + private void closeStatementAndConnection(Statement statement, boolean reuseConnection) { + if (statement == null) { + return; + } + Connection connection = null; + try { + connection = statement.getConnection(); + statement.close(); + } catch (Exception exception) { + log.error("Jdbc close statement error: {}", exception.getMessage()); + } + try { + if (!reuseConnection && connection != null) { + connection.close(); + } + } catch (Exception exception) { + log.error("Jdbc close connection error: {}", exception.getMessage()); + } + } + + /** + * Recursively decode the URL to prevent multiple encoding bypasses. + * + * @param url jdbc url + * @return decoded decoded jdbc url + */ + private String recursiveDecode(String url) { + String prev; + String decoded = url; + int max = 5; // Decode it at most 5 times to prevent infinite loops. + do { + prev = decoded; + try { + decoded = java.net.URLDecoder.decode(prev, StandardCharsets.UTF_8); + } catch (Exception e) { + break; + } + } while (!prev.equals(decoded) && --max > 0); + return decoded; + } + + /** + * construct jdbc url due the jdbc protocol + * + * @param jdbcProtocol jdbc + * @return URL + */ + private String constructDatabaseUrl(JdbcProtocol jdbcProtocol, String host, String port) { + if (Objects.nonNull(jdbcProtocol.getUrl()) + && !Objects.equals("", jdbcProtocol.getUrl()) + && jdbcProtocol.getUrl().startsWith("jdbc")) { + // limit url length + if (jdbcProtocol.getUrl().length() > 2048) { + throw new IllegalArgumentException("JDBC URL length exceeds maximum limit of 2048 characters"); + } + // remove special characters + String cleanedUrl = jdbcProtocol.getUrl().replaceAll("[\\x00-\\x1F\\x7F\\xA0]", ""); + String url = recursiveDecode(cleanedUrl); + String urlLowerCase = url.toLowerCase(); + // url format check + if (!urlLowerCase.matches("^jdbc:[a-zA-Z0-9]+:([^\\s;]+)(;[^\\s;]+)*$")) { + throw new IllegalArgumentException("Invalid JDBC URL format"); + } + // backlist check + for (String keyword : BLACK_LIST) { + if (urlLowerCase.contains(keyword.toLowerCase())) { + throw new IllegalArgumentException("Invalid JDBC URL: contains potentially malicious parameter: " + keyword); + } + } + // universal detection + String normalizedUrl = urlLowerCase.replaceAll("[\\x00-\\x1F\\x7F\\xA0]", " "); + // universal detection of JDBC injection and deserialization attacks + if (normalizedUrl.matches(".*jndi\\s*[:=].*") + || normalizedUrl.matches(".*ldap\\s*[:=].*") + || normalizedUrl.matches(".*rmi\\s*[:=].*") + || normalizedUrl.matches(".*java\\s*[:=].*") + || normalizedUrl.matches(".*serialization\\s*[:=].*") + || normalizedUrl.matches(".*deserializ.*\\s*[:=].*") + || normalizedUrl.matches(".*objectinputstream\\s*[:=].*") + || normalizedUrl.matches(".*readobject\\s*[:=].*")) { + throw new IllegalArgumentException("Invalid JDBC URL: contains potentially malicious JNDI or deserialization parameter"); + } + // universal detection of bypass + for (String pattern : UNIVERSAL_BYPASS_PATTERNS) { + if (normalizedUrl.matches(pattern)) { + throw new IllegalArgumentException("Invalid JDBC URL: contains potentially malicious bypass pattern"); + } + } + // database platform specific bypass detection + if (jdbcProtocol.getPlatform() != null) { + String platform = jdbcProtocol.getPlatform().toLowerCase(); + // check for specific bypass modes on the platform + String[] platformPatterns = PLATFORM_BYPASS_PATTERNS.get(platform); + if (platformPatterns != null) { + for (String pattern : platformPatterns) { + if (normalizedUrl.matches(pattern)) { + throw new IllegalArgumentException("Invalid " + platform.toUpperCase() + " JDBC URL: contains potentially malicious bypass pattern"); + } + } + } + } + return url; + } + assert jdbcProtocol.getPlatform() != null; + return switch (jdbcProtocol.getPlatform()) { + case "mysql", "mariadb" -> "jdbc:mysql://" + host + ":" + port + + "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase()) + + "?useUnicode=true&characterEncoding=utf-8&useSSL=false"; + case "xugu" -> "jdbc:xugu://" + host + ":" + port + + "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase()); + case "postgresql" -> "jdbc:postgresql://" + host + ":" + port + + "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase()); + case "clickhouse" -> "jdbc:clickhouse://" + host + ":" + port + + "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase()); + case "sqlserver" -> "jdbc:sqlserver://" + host + ":" + port + + ";" + (jdbcProtocol.getDatabase() == null ? "" : "DatabaseName=" + jdbcProtocol.getDatabase()) + + ";trustServerCertificate=true;"; + case "oracle" -> "jdbc:oracle:thin:@" + host + ":" + port + + "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase()); + case "dm" -> "jdbc:dm://" + host + ":" + port; + case "db2" -> "jdbc:db2://" + host + ":" + port + + "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase()); + case "testcontainers" -> "jdbc:tc:" + host + ":" + port + + ":///" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase()) + "?user=root&password=root"; + default -> throw new IllegalArgumentException("Not support database platform: " + jdbcProtocol.getPlatform()); + }; + } + + private static final class ResultSetJdbcQueryRowSet implements JdbcQueryRowSet { + + private final Statement statement; + private final java.sql.ResultSet resultSet; + private final boolean reuseConnection; + + private ResultSetJdbcQueryRowSet(Statement statement, java.sql.ResultSet resultSet, boolean reuseConnection) { + this.statement = statement; + this.resultSet = resultSet; + this.reuseConnection = reuseConnection; + } + + @Override + public boolean next() throws Exception { + return resultSet.next(); + } + + @Override + public String getString(String column) throws Exception { + return resultSet.getString(column); + } + + @Override + public String getString(int index) throws Exception { + return resultSet.getString(index); + } + + @Override + public void close() throws Exception { + Connection connection = null; + try { + connection = statement.getConnection(); + } catch (Exception ignored) { + // ignore + } + try { + resultSet.close(); + } finally { + try { + statement.close(); + } finally { + if (!reuseConnection && connection != null) { + connection.close(); + } + } + } + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/JdbcSpiLoader.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/JdbcSpiLoader.java new file mode 100644 index 0000000..a52edb0 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/JdbcSpiLoader.java @@ -0,0 +1,47 @@ +/* + * 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.collector.collect.database; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; + +/** + * load the jdbc driver first to avoid spi concurrent deadlock + */ +@Service +@Slf4j +@Order(value = Ordered.HIGHEST_PRECEDENCE) +public class JdbcSpiLoader implements CommandLineRunner { + + @Override + public void run(String... args) throws Exception { + log.info("start load jdbc drivers"); + try { + Class.forName("org.postgresql.Driver"); + Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + Class.forName("dm.jdbc.driver.DmDriver"); + Class.forName("com.clickhouse.jdbc.ClickHouseDriver"); + } catch (Exception e) { + log.error("load jdbc error: {}", e.getMessage()); + } + log.info("end load jdbc drivers"); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/query/JdbcQueryExecutor.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/query/JdbcQueryExecutor.java new file mode 100644 index 0000000..3d3c907 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/query/JdbcQueryExecutor.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.database.query; + +import org.apache.hertzbeat.common.entity.job.Metrics; + +/** + * Adapter point for replacing only the SQL query execution part of JdbcCommonCollect. + */ +public interface JdbcQueryExecutor { + + boolean supports(Metrics metrics); + + JdbcQueryRowSet executeQuery(Metrics metrics, int timeout, int maxRows) throws Exception; +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/query/JdbcQueryExecutorRegistry.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/query/JdbcQueryExecutorRegistry.java new file mode 100644 index 0000000..bb9f1c0 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/query/JdbcQueryExecutorRegistry.java @@ -0,0 +1,54 @@ +/* + * 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.collector.collect.database.query; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; +import org.apache.hertzbeat.common.entity.job.Metrics; + +/** + * Static registry used by JdbcCommonCollect to discover optional query executors. + */ +public final class JdbcQueryExecutorRegistry { + + private static final List<JdbcQueryExecutor> EXECUTORS = new CopyOnWriteArrayList<>(); + + private JdbcQueryExecutorRegistry() { + } + + public static void register(JdbcQueryExecutor executor) { + if (executor == null || EXECUTORS.contains(executor)) { + return; + } + EXECUTORS.add(executor); + } + + public static void unregister(JdbcQueryExecutor executor) { + if (executor == null) { + return; + } + EXECUTORS.remove(executor); + } + + public static Optional<JdbcQueryExecutor> resolve(Metrics metrics) { + return EXECUTORS.stream() + .filter(executor -> executor.supports(metrics)) + .findFirst(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/query/JdbcQueryRowSet.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/query/JdbcQueryRowSet.java new file mode 100644 index 0000000..a93cef1 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/database/query/JdbcQueryRowSet.java @@ -0,0 +1,33 @@ +/* + * 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.collector.collect.database.query; + +/** + * A minimal row cursor abstraction shared by JDBC and R2DBC-backed database queries. + */ +public interface JdbcQueryRowSet extends AutoCloseable { + + boolean next() throws Exception; + + String getString(String column) throws Exception; + + String getString(int index) throws Exception; + + @Override + void close() throws Exception; +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/dns/DnsCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/dns/DnsCollectImpl.java new file mode 100644 index 0000000..9c5d897 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/dns/DnsCollectImpl.java @@ -0,0 +1,231 @@ +/* + * 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.collector.collect.dns; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import java.io.IOException; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.DnsProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.springframework.util.StopWatch; +import org.xbill.DNS.DClass; +import org.xbill.DNS.Message; +import org.xbill.DNS.Name; +import org.xbill.DNS.Opcode; +import org.xbill.DNS.RRset; +import org.xbill.DNS.Rcode; +import org.xbill.DNS.Record; +import org.xbill.DNS.Resolver; +import org.xbill.DNS.Section; +import org.xbill.DNS.SimpleResolver; +import org.xbill.DNS.Type; + +/** + * dns protocol collection implementation + */ +@Slf4j +public class DnsCollectImpl extends AbstractCollect { + /* + each part of dig command output + */ + private static final String HEADER = "header"; + private static final String QUESTION = "question"; + private static final String ANSWER = "answer"; + private static final String AUTHORITY = "authority"; + private static final String ADDITIONAL = "additional"; + /* + * used for header key + * example: + * ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3221 + * ;; flags: qr rd ra ; qd: 1 an: 1 au: 0 ad: 0 + * + * + * opcode -> opcode + * status -> status + * flags -> flags + * qd -> questionRowCount + * an -> answerRowCount + * au -> authorityRowCount + * ad -> additionalRowCount + */ + private static final String RESPONSE_TIME = "responseTime"; + private static final String OP_CODE = "opcode"; + private static final String STATUS = "status"; + private static final String FLAGS = "flags"; + private static final String QUESTION_ROW_COUNT = "questionRowCount"; + private static final String ANSWER_ROW_COUNT = "answerRowCount"; + private static final String AUTHORITY_ROW_COUNT = "authorityRowCount"; + private static final String ADDITIONAL_ROW_COUNT = "additionalRowCount"; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + // compatible with monitoring template configurations of older versions + if (StringUtils.isBlank(metrics.getDns().getQueryClass())) { + metrics.getDns().setQueryClass(DClass.string(DClass.IN)); + } + // check params + if (checkDnsProtocolFailed(metrics.getDns())) { + throw new IllegalArgumentException("DNS collect must have a valid DNS protocol param! "); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + + DnsResolveResult dnsResolveResult; + try { + // run dig command + dnsResolveResult = dig(metrics.getDns()); + } catch (IOException e) { + log.info(CommonUtil.getMessageFromThrowable(e)); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(e.getMessage()); + return; + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn("[dns collect] error: {}", e.getMessage(), e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + return; + } + + // build dns metrics data + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + if (StringUtils.equals(HEADER, metrics.getName())) { + // add header columns + Map<String, String> headerInfo = dnsResolveResult.getHeaderInfo(); + metrics.getAliasFields().forEach(field -> valueRowBuilder.addColumn(headerInfo.getOrDefault(field, CommonConstants.NULL_VALUE))); + } else { + // add question/answer/authority/additional columns + List<String> currentMetricsResolveResultList = dnsResolveResult.getList(metrics.getName()); + for (int index = 0; index < metrics.getAliasFields().size(); index++) { + valueRowBuilder.addColumn(index >= currentMetricsResolveResultList.size() + ? CommonConstants.NULL_VALUE + : currentMetricsResolveResultList.get(index)); + } + } + + builder.addValueRow(valueRowBuilder.build()); + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_DNS; + } + + private boolean checkDnsProtocolFailed(DnsProtocol dnsProtocol) { + return Objects.isNull(dnsProtocol) || dnsProtocol.isInvalid(); + } + + /** + * run dig command + */ + private DnsResolveResult dig(DnsProtocol dns) throws IOException { + StopWatch responseTimeStopWatch = new StopWatch("responseTime"); + responseTimeStopWatch.start(); + + Name name = Name.fromString(dns.getAddress(), Name.root); + Message query = Message.newQuery(Record.newRecord(name, Type.value(dns.getRecordType()), DClass.value(dns.getQueryClass()))); + Resolver res = new SimpleResolver(dns.getDnsServerIP()); + res.setTimeout(Duration.of(Long.parseLong(dns.getTimeout()), ChronoUnit.MILLIS)); + res.setTCP(Boolean.parseBoolean(dns.getTcp())); + res.setPort(Integer.parseInt(dns.getPort())); + + Message response = res.send(query); + responseTimeStopWatch.stop(); + return resolve(response, responseTimeStopWatch.lastTaskInfo().getTimeMillis()); + } + + private DnsResolveResult resolve(Message message, Long responseTime) { + return DnsResolveResult.builder() + .headerInfo(getHeaderInfo(message, responseTime)) + .questionList(getSectionInfo(message, Section.QUESTION)) + .answerList(getSectionInfo(message, Section.ANSWER)) + .authorityList(getSectionInfo(message, Section.AUTHORITY)) + .additionalList(getSectionInfo(message, Section.ADDITIONAL)) + .build(); + } + + private Map<String, String> getHeaderInfo(Message message, Long responseTime) { + Map<String, String> resultMap = Maps.newHashMap(); + resultMap.put(RESPONSE_TIME, String.valueOf(responseTime)); + resultMap.put(OP_CODE, Opcode.string(message.getHeader().getOpcode())); + resultMap.put(STATUS, Rcode.string(message.getHeader().getRcode())); + resultMap.put(FLAGS, message.getHeader().printFlags()); + resultMap.put(QUESTION_ROW_COUNT, String.valueOf(message.getHeader().getCount(Section.QUESTION))); + resultMap.put(ANSWER_ROW_COUNT, String.valueOf(message.getHeader().getCount(Section.ANSWER))); + resultMap.put(AUTHORITY_ROW_COUNT, String.valueOf(message.getHeader().getCount(Section.AUTHORITY))); + resultMap.put(ADDITIONAL_ROW_COUNT, String.valueOf(message.getHeader().getCount(Section.ADDITIONAL))); + + return resultMap; + } + + private List<String> getSectionInfo(Message message, int section) { + List<RRset> currentSetList = message.getSectionRRsets(section); + + if (CollectionUtils.isEmpty(currentSetList)) { + return Lists.newArrayList(); + } + + List<String> infoList = Lists.newArrayListWithCapacity(currentSetList.size()); + currentSetList.forEach(res -> infoList.add(res.toString())); + + return infoList; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + private static class DnsResolveResult { + private Map<String, String> headerInfo; + /** example: www.google.com. 140 IN A 192.133.77.133 **/ + private List<String> questionList; + private List<String> answerList; + private List<String> authorityList; + private List<String> additionalList; + + public List<String> getList(String metricsName) { + return switch (metricsName) { + case QUESTION -> questionList; + case ANSWER -> answerList; + case AUTHORITY -> authorityList; + case ADDITIONAL -> additionalList; + default -> Collections.emptyList(); + }; + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImpl.java new file mode 100644 index 0000000..03307e1 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImpl.java @@ -0,0 +1,232 @@ +/* + * 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.collector.collect.ftp; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.net.ftp.FTPClient; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.FtpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.sftp.client.SftpClient; +import org.apache.sshd.sftp.client.SftpClientFactory; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * ftp protocol collection implementation + */ +@Slf4j +public class FtpCollectImpl extends AbstractCollect { + + private static final String ANONYMOUS = "anonymous"; + private static final String PASSWORD = "password"; + + /** + * preCheck params + */ + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException{ + if (metrics == null || metrics.getFtp() == null) { + throw new IllegalArgumentException("Ftp collect must has ftp params."); + } + FtpProtocol ftpProtocol = metrics.getFtp(); + Assert.hasText(ftpProtocol.getHost(), "Ftp Protocol host is required."); + Assert.hasText(ftpProtocol.getPort(), "Ftp Protocol port is required."); + Assert.hasText(ftpProtocol.getDirection(), "Ftp Protocol direction is required."); + Assert.hasText(ftpProtocol.getTimeout(), "Ftp Protocol timeout is required."); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + boolean ssl = Boolean.parseBoolean(metrics.getFtp().getSsl()); + if (ssl){ + handleSftpCollect(builder, metrics); + } else { + handleFtpCollect(builder, metrics); + } + } + + /** + * collect data: key-value + * Please modify this, if you want to add some metrics. + */ + private Map<String, String> collectValue(FTPClient ftpClient, FtpProtocol ftpProtocol) { + boolean isActive; + String responseTime; + try { + long startTime = System.currentTimeMillis(); + connect(ftpClient, ftpProtocol); + login(ftpClient, ftpProtocol); + // In here, we can do some extended operation without changing the architecture + isActive = ftpClient.changeWorkingDirectory(ftpProtocol.getDirection()); + long endTime = System.currentTimeMillis(); + responseTime = String.valueOf(endTime - startTime); + ftpClient.disconnect(); + } catch (Exception e) { + log.info("[FTPClient] error: {}", CommonUtil.getMessageFromThrowable(e), e); + throw new IllegalArgumentException(e.getMessage()); + } + return new HashMap<>(8) { + { + put("isActive", Boolean.toString(isActive)); + put("responseTime", responseTime); + } + }; + } + + private Map<String, String> collectValue(SftpClient sftpClient, FtpProtocol ftpProtocol) { + boolean isActive; + String responseTime; + try { + long startTime = System.currentTimeMillis(); + sftpClient.stat(ftpProtocol.getDirection()); + isActive = true; + long endTime = System.currentTimeMillis(); + responseTime = String.valueOf(endTime - startTime); + } catch (IOException e) { + throw new IllegalArgumentException("[SFTPClient] error: {}" + CommonUtil.getMessageFromThrowable(e), e); + } + return Map.of("isActive", Boolean.toString(isActive), "responseTime", responseTime); + } + + /** + * ftp login + */ + private void login(FTPClient ftpClient, FtpProtocol ftpProtocol) { + try { + // username: not empty, password: not empty + if (StringUtils.hasText(ftpProtocol.getUsername()) && StringUtils.hasText(ftpProtocol.getPassword())) { + if (!ftpClient.login(ftpProtocol.getUsername(), ftpProtocol.getPassword())) { + throw new IllegalArgumentException("The username or password may be wrong."); + } + return; + } + // anonymous access + if (!ftpClient.login(ANONYMOUS, PASSWORD)) { + throw new IllegalArgumentException("The server may not allow anonymous access, we need to username and password."); + } + } catch (Exception e) { + log.info("[ftp login] error: {}", CommonUtil.getMessageFromThrowable(e), e); + throw new IllegalArgumentException(e.getMessage()); + } + } + + /** + * connect + */ + private void connect(FTPClient ftpClient, FtpProtocol ftpProtocol) { + try { + ftpClient.connect(ftpProtocol.getHost(), Integer.parseInt(ftpProtocol.getPort())); + } catch (Exception e) { + log.info("[ftp connection] error: {}", CommonUtil.getMessageFromThrowable(e), e); + throw new IllegalArgumentException("The host or port may be wrong."); + } + } + + private ClientSession connect(SshClient client, FtpProtocol ftpProtocol) { + client.start(); + try { + ClientSession session = client.connect(ftpProtocol.getUsername(), ftpProtocol.getHost(), Integer.parseInt(ftpProtocol.getPort())) + .verify(Integer.parseInt(ftpProtocol.getTimeout())) + .getSession(); + session.addPasswordIdentity(ftpProtocol.getPassword()); + session.auth().verify(Integer.parseInt(ftpProtocol.getTimeout())); + return session; + } catch (Exception e) { + throw new IllegalArgumentException("[sftp connection] error: {}" + CommonUtil.getMessageFromThrowable(e), e); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_FTP; + } + + private void handleFtpCollect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + FTPClient ftpClient = new FTPClient(); + FtpProtocol ftpProtocol = metrics.getFtp(); + // Set timeout + ftpClient.setControlKeepAliveReplyTimeout(Integer.parseInt(ftpProtocol.getTimeout())); + + // Collect data to load in CollectRep.ValueRow.Builder's object + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + Map<String, String> valueMap; + try { + valueMap = collectValue(ftpClient, ftpProtocol); + metrics.getAliasFields().forEach(it -> { + if (valueMap.containsKey(it)) { + String fieldValue = valueMap.get(it); + valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + }); + } catch (Exception e) { + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(e.getMessage()); + return; + } + builder.addValueRow(valueRowBuilder.build()); + } + + private void handleSftpCollect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + FtpProtocol ftpProtocol = metrics.getFtp(); + ClientSession session = null; + SftpClient sftpClient = null; + SshClient client = null; + try { + client = SshClient.setUpDefaultClient(); + session = connect(client, ftpProtocol); + sftpClient = SftpClientFactory.instance().createSftpClient(session); + Map<String, String> valueMap = collectValue(sftpClient, ftpProtocol); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + metrics.getAliasFields().forEach(it -> + valueRowBuilder.addColumn(valueMap.getOrDefault(it, CommonConstants.NULL_VALUE)) + ); + builder.addValueRow(valueRowBuilder.build()); + } catch (Exception e) { + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(e.getMessage()); + } finally { + try { + if (sftpClient != null && sftpClient.isOpen()){ + sftpClient.close(); + } + if (session != null && session.isOpen()){ + session.close(); + } + if (client != null && client.isOpen()){ + client.close(); + } + } catch (Exception e){ + log.error("[SFTPClient] error while closing: {}", CommonUtil.getMessageFromThrowable(e), e); + } + } + } +} \ No newline at end of file diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImpl.java new file mode 100644 index 0000000..ab9f2dc --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImpl.java @@ -0,0 +1,1010 @@ +/* + * 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.collector.collect.http; + +import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.StringReader; +import java.net.ConnectException; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import javax.net.ssl.SSLException; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.collector.collect.http.promethus.AbstractPrometheusParse; +import org.apache.hertzbeat.collector.collect.http.promethus.PrometheusParseCreator; +import org.apache.hertzbeat.collector.collect.prometheus.parser.MetricFamily; +import org.apache.hertzbeat.collector.collect.prometheus.parser.OnlineParser; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.collector.util.JsonPathParser; +import org.apache.hertzbeat.collector.util.TimeExpressionUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.Base64Util; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriUtils; +import org.xml.sax.InputSource; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.HashSet; +import java.util.ArrayList; +import java.util.Collections; + +/** + * http https collect + */ +@Slf4j +public class HttpCollectImpl extends AbstractCollect { + + /** + * Pre-compiled regex patterns for dangerous Xpath detection. + * Compiled once at class load for performance. + */ + private static final List<Pattern> DANGEROUS_XPATH_PATTERNS; + + private static final Pattern XML_ENCODING_PATTERN = Pattern.compile( + "<\\?xml\\s+[^>]*encoding\\s*=\\s*[\"']([^\"']+)[\"']", + Pattern.CASE_INSENSITIVE); + + static { + List<Pattern> patterns = new ArrayList<>(); + for (String pattern : CollectorConstants.DANGEROUS_XPATH_PATTERNS) { + // Use CASE_INSENSITIVE to prevent bypass via case variations (e.g., //TEXT() vs //text()) + patterns.add(Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); + } + DANGEROUS_XPATH_PATTERNS = Collections.unmodifiableList(patterns); + } + + private final Set<Integer> defaultSuccessStatusCodes = Set.of( + HttpStatus.SC_OK, + HttpStatus.SC_CREATED, + HttpStatus.SC_ACCEPTED, + HttpStatus.SC_MULTIPLE_CHOICES, + HttpStatus.SC_MOVED_PERMANENTLY, + HttpStatus.SC_MOVED_TEMPORARILY); + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getHttp() == null) { + throw new IllegalArgumentException("Http/Https collect must has http params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + + HttpProtocol httpProtocol = metrics.getHttp(); + String url = httpProtocol.getUrl(); + if (!StringUtils.hasText(url) || !url.startsWith(RIGHT_DASH)) { + httpProtocol.setUrl(StringUtils.hasText(url) ? RIGHT_DASH + url.trim() : RIGHT_DASH); + } + if (CollectionUtils.isEmpty(httpProtocol.getSuccessCodes())) { + httpProtocol.setSuccessCodes(List.of(HttpStatus.SC_OK + "")); + } + + HttpContext httpContext = createHttpContext(metrics.getHttp()); + HttpUriRequest request = createHttpRequest(metrics.getHttp()); + try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) { + int statusCode = response.getStatusLine().getStatusCode(); + boolean isSuccessInvoke = checkSuccessInvoke(metrics, statusCode); + log.debug("http response status: {}", statusCode); + if (!isSuccessInvoke) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode); + return; + } + + long responseTime = System.currentTimeMillis() - startTime; + String parseType = metrics.getHttp().getParseType(); + HttpEntity entity = response.getEntity(); + + try { + if (DispatchConstants.PARSE_PROMETHEUS.equals(parseType)) { + if (entity != null) { + parseResponseByPrometheusExporter(entity.getContent(), metrics.getAliasFields(), builder); + } + } else if (DispatchConstants.PARSE_HEADER.equals(parseType)) { + parseResponseByHeader(builder, metrics.getAliasFields(), response); + // Consume entity to release connection + EntityUtils.consumeQuietly(entity); + } else { + /* + this could create large objects, potentially impacting JVM memory space significantly. + Option 1: Parse using InputStream, but this requires significant code changes; + Option 2: Manually trigger garbage collection, similar to how it's done in Dubbo for large inputs. + */ + String resp = entity == null ? "" : readEntityAsString(entity, isXmlParseType(parseType)); + if (!StringUtils.hasText(resp)) { + log.info("http response entity is empty, status: {}.", statusCode); + } + switch (parseType) { + case DispatchConstants.PARSE_JSON_PATH -> + parseResponseByJsonPath(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime); + case DispatchConstants.PARSE_PROM_QL -> + parseResponseByPromQl(resp, metrics.getAliasFields(), metrics.getHttp(), builder); + case DispatchConstants.PARSE_XML_PATH -> + parseResponseByXmlPath(resp, metrics, builder, responseTime); + case DispatchConstants.PARSE_WEBSITE -> + parseResponseByWebsite(resp, metrics, metrics.getHttp(), builder, responseTime, statusCode); + case DispatchConstants.PARSE_SITE_MAP -> + parseResponseBySiteMap(resp, metrics.getAliasFields(), builder); + case DispatchConstants.PARSE_CONFIG -> + parseResponseByConfig(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime); + default -> + parseResponseByDefault(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime); + } + } + } catch (Exception e) { + log.info("parse error: {}.", e.getMessage(), e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("parse response data error:" + e.getMessage()); + } + } catch (ClientProtocolException e1) { + String errorMsg = CommonUtil.getMessageFromThrowable(e1); + log.error(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (UnknownHostException e2) { + String errorMsg = CommonUtil.getMessageFromThrowable(e2); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_REACHABLE); + builder.setMsg("unknown host:" + errorMsg); + } catch (InterruptedIOException | ConnectException | SSLException e3) { + String errorMsg = CommonUtil.getMessageFromThrowable(e3); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (IOException e4) { + String errorMsg = CommonUtil.getMessageFromThrowable(e4); + log.info(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error(errorMsg, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (request != null) { + request.abort(); + } + } + } + + private boolean isXmlParseType(String parseType) { + return DispatchConstants.PARSE_XML_PATH.equals(parseType) || DispatchConstants.PARSE_SITE_MAP.equals(parseType); + } + + private String readEntityAsString(HttpEntity entity, boolean xmlAware) throws IOException { + if (!xmlAware) { + return EntityUtils.toString(entity, StandardCharsets.UTF_8); + } + byte[] content = EntityUtils.toByteArray(entity); + ContentType contentType = ContentType.get(entity); + Charset charset = contentType == null ? null : contentType.getCharset(); + if (charset == null) { + charset = detectXmlCharset(content); + } + return new String(content, charset); + } + + private Charset detectXmlCharset(byte[] content) { + int previewLength = Math.min(content.length, 256); + String preview = new String(content, 0, previewLength, StandardCharsets.US_ASCII); + Matcher matcher = XML_ENCODING_PATTERN.matcher(preview); + if (matcher.find()) { + try { + return Charset.forName(matcher.group(1)); + } catch (IllegalArgumentException ignored) { + return StandardCharsets.UTF_8; + } + } + return StandardCharsets.UTF_8; + } + + private void parseResponseByHeader(CollectRep.MetricsData.Builder builder, List<String> aliases, CloseableHttpResponse response) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliases) { + if (!StringUtils.hasText(alias)) { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + continue; + } + final Header firstHeader = response.getFirstHeader(alias); + if (Objects.isNull(firstHeader)) { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + continue; + } + + valueRowBuilder.addColumn(firstHeader.getValue()); + } + builder.addValueRow(valueRowBuilder.build()); + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_HTTP; + } + + private void parseResponseByWebsite(String resp, Metrics metrics, HttpProtocol http, + CollectRep.MetricsData.Builder builder, Long responseTime, int statusCode) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword()); + for (String alias : metrics.getAliasFields()) { + if (CollectorConstants.STATUS_CODE.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Integer.toString(statusCode)); + } else { + addColumnForSummary(responseTime, valueRowBuilder, keywordNum, alias); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + + private void addColumnForSummary(Long responseTime, CollectRep.ValueRow.Builder valueRowBuilder, int keywordNum, String alias) { + if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(responseTime.toString()); + } else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Integer.toString(keywordNum)); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + + private void parseResponseBySiteMap(String resp, List<String> aliasFields, + CollectRep.MetricsData.Builder builder) { + List<String> siteUrls = new LinkedList<>(); + boolean isXmlFormat = true; + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + // see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + dbf.setXIncludeAware(false); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document document = db.parse(new InputSource(new StringReader(resp))); + NodeList urlList = document.getElementsByTagName("url"); + for (int i = 0; i < urlList.getLength(); i++) { + Node urlNode = urlList.item(i); + NodeList childNodes = urlNode.getChildNodes(); + for (int k = 0; k < childNodes.getLength(); k++) { + Node currentNode = childNodes.item(k); + // distinguish between text nodes and element nodes + if (currentNode.getNodeType() == Node.ELEMENT_NODE && "loc".equals(currentNode.getNodeName())) { + // retrieves the value of the loc node + siteUrls.add(currentNode.getFirstChild().getNodeValue()); + break; + } + } + } + } catch (Exception e) { + log.warn(e.getMessage()); + isXmlFormat = false; + } + // if XML parsing fails, parse in TXT format + if (!isXmlFormat) { + try { + String[] urls = resp.split("\n"); + // validate whether the given value is a URL + if (IpDomainUtil.isHasSchema(urls[0])) { + siteUrls.addAll(Arrays.asList(urls)); + } + } catch (Exception e) { + log.warn(e.getMessage(), e); + } + } + // start looping through each site URL to collect its HTTP status code, response time, and exception information + for (String siteUrl : siteUrls) { + String errorMsg = ""; + Integer statusCode = null; + long startTime = System.currentTimeMillis(); + try { + HttpGet httpGet = new HttpGet(siteUrl); + try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(httpGet)) { + statusCode = response.getStatusLine().getStatusCode(); + EntityUtils.consume(response.getEntity()); + } + } catch (ClientProtocolException e1) { + if (e1.getCause() != null) { + errorMsg = e1.getCause().getMessage(); + } else { + errorMsg = e1.getMessage(); + } + } catch (UnknownHostException e2) { + errorMsg = "unknown host"; + } catch (InterruptedIOException | ConnectException | SSLException e3) { + errorMsg = "connect error: " + e3.getMessage(); + } catch (IOException e4) { + errorMsg = "io error: " + e4.getMessage(); + } catch (Exception e) { + errorMsg = "error: " + e.getMessage(); + } + long responseTime = System.currentTimeMillis() - startTime; + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (NetworkConstants.URL.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(siteUrl); + } else if (NetworkConstants.STATUS_CODE.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(statusCode == null + ? CommonConstants.NULL_VALUE : String.valueOf(statusCode)); + } else if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(String.valueOf(responseTime)); + } else if (NetworkConstants.ERROR_MSG.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(errorMsg); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + + /** + * Validates the Xpath expression to prevent DoS attacks. + * Checks for dangerous patterns that could traverse the entire XML document. + * Uses pre-compiled patterns for performance and case-insensitive matching for security. + * + * @param xpathExpression the Xpath expression to validate + * @throws IllegalArgumentException if the expression contains dangerous patterns + */ + private void validateXpathExpression(String xpathExpression) throws IllegalArgumentException { + if (!StringUtils.hasText(xpathExpression)) { + return; + } + + // Check against dangerous patterns using pre-compiled regex + for (Pattern pattern : DANGEROUS_XPATH_PATTERNS) { + Matcher matcher = pattern.matcher(xpathExpression); + if (matcher.find()) { + throw new IllegalArgumentException( + "Xpath expression contains dangerous pattern that may cause DoS: " + pattern.pattern() + ); + } + } + + // Check for excessive wildcard usage (more than 3 // or * operators) + long descendantAxisCount = xpathExpression.chars().filter(ch -> ch == '/').count(); + long wildcardCount = xpathExpression.chars().filter(ch -> ch == '*').count(); + + if (descendantAxisCount > 10 || wildcardCount > 5) { + throw new IllegalArgumentException( + "Xpath expression contains too many wildcards or descendant axes, potential DoS risk" + ); + } + + log.debug("Xpath expression validation passed: {}", xpathExpression); + } + + private void parseResponseByXmlPath(String resp, Metrics metrics, + CollectRep.MetricsData.Builder builder, Long responseTime) { + HttpProtocol http = metrics.getHttp(); + List<String> aliasFields = metrics.getAliasFields(); + String xpathExpression = http.getParseScript(); + + // Layer 1: Validate Xpath expression is not empty + if (!StringUtils.hasText(xpathExpression)) { + log.warn("Http collect parse type is xmlPath, but the xpath expression is empty."); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Xpath expression is empty"); + return; + } + + // Layer 2: Check XML response size to prevent memory exhaustion + if (resp != null && resp.length() > CollectorConstants.MAX_XML_RESPONSE_SIZE) { + log.warn("XML response size {} bytes exceeds maximum allowed size {} bytes", + resp.length(), CollectorConstants.MAX_XML_RESPONSE_SIZE); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("XML response exceeds maximum allowed size of " + + (CollectorConstants.MAX_XML_RESPONSE_SIZE / 1024 / 1024) + "MB"); + return; + } + + // Layer 3: Validate Xpath expression for dangerous patterns + try { + validateXpathExpression(xpathExpression); + } catch (IllegalArgumentException e) { + log.warn("Xpath expression validation failed: {}", e.getMessage()); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(e.getMessage()); + return; + } + + int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword()); + + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + + // Layer 4: Enable XML secure processing and XXE protection + dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + dbf.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + + DocumentBuilder db = dbf.newDocumentBuilder(); + Document document = db.parse(new InputSource(new StringReader(resp))); + + XPathFactory xpathFactory = XPathFactory.newInstance(); + XPath xpath = xpathFactory.newXPath(); + + NodeList nodeList = (NodeList) xpath.evaluate(xpathExpression, document, XPathConstants.NODESET); + + if (nodeList == null || nodeList.getLength() == 0) { + log.debug("Xpath expression '{}' returned no nodes.", xpathExpression); + boolean requestedSummaryFields = aliasFields.stream() + .anyMatch(alias -> NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias) + || CollectorConstants.KEYWORD.equalsIgnoreCase(alias)); + + if (requestedSummaryFields) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(responseTime.toString()); + } else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Integer.toString(keywordNum)); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + return; + } + + // Layer 5: Limit the number of results to prevent excessive resource consumption + int resultSize = nodeList.getLength(); + int maxResults = CollectorConstants.MAX_XPATH_RESULT_NODES; + if (resultSize > maxResults) { + log.warn("Xpath expression returned {} nodes, exceeding limit of {}. Processing first {} nodes only.", + resultSize, maxResults, maxResults); + resultSize = maxResults; + } + + for (int i = 0; i < resultSize; i++) { + Node node = nodeList.item(i); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + + for (String alias : aliasFields) { + if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(responseTime.toString()); + } else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Integer.toString(keywordNum)); + } else { + try { + String value = (String) xpath.evaluate(alias, node, XPathConstants.STRING); + valueRowBuilder.addColumn(StringUtils.hasText(value) ? value : CommonConstants.NULL_VALUE); + } catch (XPathExpressionException e) { + log.warn("Failed to evaluate Xpath '{}' for node [{}]: {}", alias, node.getNodeName(), e.getMessage()); + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + } + builder.addValueRow(valueRowBuilder.build()); + } + + } catch (Exception e) { + log.warn("Failed to parse XML response with Xpath '{}': {}", xpathExpression, e.getMessage(), e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Failed to parse XML response: " + e.getMessage()); + } + } + + + + /** + * Parses the response body in Properties/Config format. + * Two modes are supported: + * 1. single-object mode: if http.parseScript is null, aliasFields are treated as indicator names. + * - If there is a locator in the indicator definition, use the locator as the key of the Properties. + * - Otherwise, use aliasField (metric name) as the key for Properties. + * Generate a single row of data. + * 2. array mode: if http.parseScript is not empty (e.g. “users”), treat it as an array base path. + * Treat aliasFields as the attribute name of an array element, and generate a single row of data for each array index. locator is invalid in this mode. + * + * @param resp Response body string + * @param aliasFields List of metrics aliases (i.e., the list of fields in metrics.fields). + * @param http http protocol configuration + * @param builder The metrics data builder. + * @param responseTime response time + */ + private void parseResponseByConfig(String resp, List<String> aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder, Long responseTime) { + if (!StringUtils.hasText(resp)) { + log.warn("Http collect parse type is config, but response body is empty."); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Response body is empty"); + return; + } + + Properties properties = new Properties(); + try (StringReader reader = new StringReader(resp)) { + properties.load(reader); + } catch (IOException e) { + log.warn("Failed to parse config response: {}", e.getMessage(), e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Failed to parse config response: " + e.getMessage()); + return; + } + String arrayBasePath = http.getParseScript(); + int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword()); + + if (!StringUtils.hasText(arrayBasePath)) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(responseTime.toString()); + } else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Integer.toString(keywordNum)); + } else { + String value = properties.getProperty(alias); + valueRowBuilder.addColumn(value != null ? value : CommonConstants.NULL_VALUE); + } + } + CollectRep.ValueRow valueRow = valueRowBuilder.build(); + if (hasMeaningfulDataInRow(valueRow, aliasFields)) { + builder.addValueRow(valueRow); + } else { + log.warn("No meaningful data found in single config object response for aliasFields: {}", aliasFields); + } + } else { + Pattern pattern = Pattern.compile("^" + Pattern.quote(arrayBasePath) + "\\[(\\d+)]\\."); + Set<Integer> existingIndices = new HashSet<>(); + for (String key : properties.stringPropertyNames()) { + Matcher matcher = pattern.matcher(key); + if (matcher.find()) { + try { + int index = Integer.parseInt(matcher.group(1)); + existingIndices.add(index); + } catch (NumberFormatException e) { + log.error("Could not parse index from key: {}", key); + } + } + } + if (existingIndices.isEmpty()) { + log.warn("Could not find any array elements for base path '{}' in config response.", arrayBasePath); + return; + } + List<Integer> sortedIndices = new ArrayList<>(existingIndices); + Collections.sort(sortedIndices); + for (int i : sortedIndices) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(responseTime.toString()); + } else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Integer.toString(keywordNum)); + } else { + String currentKey = arrayBasePath + "[" + i + "]." + alias; + String value = properties.getProperty(currentKey); + valueRowBuilder.addColumn(value != null ? value : CommonConstants.NULL_VALUE); + } + } + CollectRep.ValueRow valueRow = valueRowBuilder.build(); + if (hasMeaningfulDataInRow(valueRow, aliasFields)) { + builder.addValueRow(valueRowBuilder.build()); + } + } + } + } + + private boolean hasMeaningfulDataInRow(CollectRep.ValueRow valueRow, List<String> aliasFields) { + if (valueRow.getColumnsCount() == 0) { + return false; + } + if (valueRow.getColumnsCount() != aliasFields.size()) { + log.error("Column count ({}) mismatch with aliasFields size ({}) when checking meaningful data.", + valueRow.getColumnsCount(), aliasFields.size()); + return false; + } + + boolean hasMeaningfulData = false; + for (int i = 0; i < valueRow.getColumnsCount(); i++) { + String columnValue = valueRow.getColumns(i); + String alias = aliasFields.get(i); + if (!CommonConstants.NULL_VALUE.equals(columnValue) && (!NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias) && !CollectorConstants.KEYWORD.equalsIgnoreCase(alias))) { + hasMeaningfulData = true; + break; + } + if ((NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias) || CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) && !CommonConstants.NULL_VALUE.equals(columnValue)) { + hasMeaningfulData = true; + } + } + return hasMeaningfulData; + } + + private void parseResponseByJsonPath(String resp, List<String> aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder, Long responseTime) { + List<Object> results = JsonPathParser.parseContentWithJsonPath(resp, http.getParseScript()); + int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword()); + for (int i = 0; i < results.size(); i++) { + Object objectValue = results.get(i); + // if a property is missing or empty due to target version issues, filter it. Refer to the app-elasticsearch.yml configuration under name: nodes + if (objectValue == null) { + continue; + } + if (objectValue instanceof Map) { + Map<String, Object> stringMap = (Map<String, Object>) objectValue; + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + Object value = stringMap.get(alias); + if (value != null) { + valueRowBuilder.addColumn(String.valueOf(value)); + } else { + if (alias.startsWith("$.")) { + List<Object> subResults = JsonPathParser.parseContentWithJsonPath(resp, http.getParseScript() + alias.substring(1)); + if (subResults != null && subResults.size() > i) { + Object resultValue = subResults.get(i); + valueRowBuilder.addColumn(resultValue == null ? CommonConstants.NULL_VALUE : String.valueOf(resultValue)); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } else { + addColumnForSummary(responseTime, valueRowBuilder, keywordNum, alias); + } + } + } + builder.addValueRow(valueRowBuilder.build()); + } else if (objectValue instanceof String stringValue) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(responseTime.toString()); + } else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Integer.toString(keywordNum)); + } else { + valueRowBuilder.addColumn(stringValue); + } + } + builder.addValueRow(valueRowBuilder.build()); + } else if (objectValue instanceof Number numberValue) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(responseTime.toString()); + } else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Integer.toString(keywordNum)); + } else { + valueRowBuilder.addColumn(numberValue.toString()); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + } + + private void parseResponseByPromQl(String resp, List<String> aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder) { + AbstractPrometheusParse prometheusParser = PrometheusParseCreator.getPrometheusParse(); + prometheusParser.handle(resp, aliasFields, http, builder); + } + + private void parseResponseByPrometheusExporter(InputStream content, List<String> aliasFields, CollectRep.MetricsData.Builder builder) throws IOException { + String metrics = builder.getMetrics(); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(content, metrics); + if (metricFamilyMap == null || metricFamilyMap.isEmpty()) { + return; + } + MetricFamily metricFamily = metricFamilyMap.get(metrics); + if (null == metricFamily || CollectionUtils.isEmpty(metricFamily.getMetricList())) { + return; + } + for (MetricFamily.Metric metric : metricFamily.getMetricList()) { + Map<String, String> labelMap = metric.getLabels() + .stream() + .collect(Collectors.toMap(MetricFamily.Label::getName, MetricFamily.Label::getValue)); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String aliasField : aliasFields) { + String columnValue = labelMap.get(aliasField); + if (columnValue != null) { + valueRowBuilder.addColumn(columnValue); + } else if (CommonConstants.PROM_VALUE.equals(aliasField) || CommonConstants.PROM_METRIC_VALUE.equals(aliasField)) { + valueRowBuilder.addColumn(String.valueOf(metric.getValue())); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + + + private void parseResponseByDefault(String resp, List<String> aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder, Long responseTime) { + JsonElement element = JsonParser.parseString(resp); + int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword()); + if (element.isJsonArray()) { + JsonArray array = element.getAsJsonArray(); + for (JsonElement jsonElement : array) { + getValueFromJson(aliasFields, builder, responseTime, jsonElement, keywordNum); + } + } else { + getValueFromJson(aliasFields, builder, responseTime, element, keywordNum); + } + } + + private void getValueFromJson(List<String> aliasFields, CollectRep.MetricsData.Builder builder, Long responseTime, JsonElement element, int keywordNum) { + if (element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + JsonElement valueElement = object.get(alias); + if (valueElement != null) { + String value = valueElement.getAsString(); + valueRowBuilder.addColumn(value); + } else { + addColumnForSummary(responseTime, valueRowBuilder, keywordNum, alias); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + + /** + * create httpContext + * + * @param httpProtocol http protocol + * @return context + */ + public HttpContext createHttpContext(HttpProtocol httpProtocol) { + HttpProtocol.Authorization auth = httpProtocol.getAuthorization(); + if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) { + HttpClientContext clientContext = new HttpClientContext(); + if (StringUtils.hasText(auth.getDigestAuthUsername()) + && StringUtils.hasText(auth.getDigestAuthPassword())) { + CredentialsProvider provider = new BasicCredentialsProvider(); + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(auth.getDigestAuthUsername(), + auth.getDigestAuthPassword()); + AuthScope authScope = new AuthScope(httpProtocol.getHost(), Integer.parseInt(httpProtocol.getPort())); + provider.setCredentials(authScope, credentials); + + clientContext.setCredentialsProvider(provider); + return clientContext; + } + } + return null; + } + + /** + * create http request + * + * @param httpProtocol http params + * @return http uri request + */ + public HttpUriRequest createHttpRequest(HttpProtocol httpProtocol) { + RequestBuilder requestBuilder; + String httpMethod = httpProtocol.getMethod().toUpperCase(); + if (HttpMethod.GET.matches(httpMethod)) { + requestBuilder = RequestBuilder.get(); + } else if (HttpMethod.POST.matches(httpMethod)) { + requestBuilder = RequestBuilder.post(); + } else if (HttpMethod.PUT.matches(httpMethod)) { + requestBuilder = RequestBuilder.put(); + } else if (HttpMethod.DELETE.matches(httpMethod)) { + requestBuilder = RequestBuilder.delete(); + } else if (HttpMethod.PATCH.matches(httpMethod)) { + requestBuilder = RequestBuilder.patch(); + } else { + // not support the method + log.error("not support the http method: {}.", httpProtocol.getMethod()); + return null; + } + // params + Map<String, String> params = httpProtocol.getParams(); + boolean enableUrlEncoding = Boolean.parseBoolean(httpProtocol.getEnableUrlEncoding()); + StringBuilder queryParams = new StringBuilder(); + + if (params != null && !params.isEmpty()) { + for (Map.Entry<String, String> param : params.entrySet()) { + String key = param.getKey(); + String value = param.getValue(); + + if (!StringUtils.hasText(key)) { + continue; + } + + if (!queryParams.isEmpty()) { + queryParams.append("&"); + } + + if (enableUrlEncoding) { + key = UriUtils.encodeQueryParam(key, "UTF-8"); + } + queryParams.append(key); + + if (StringUtils.hasText(value)) { + String calculatedValue = TimeExpressionUtil.calculate(value); + if (enableUrlEncoding) { + calculatedValue = UriUtils.encodeQueryParam(calculatedValue, "UTF-8"); + } + queryParams.append("=").append(calculatedValue); + } + } + } + + // The default request header can be overridden if customized + // keep-alive + requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE); + requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT); + // headers The custom request header is overwritten here + Map<String, String> headers = httpProtocol.getHeaders(); + if (headers != null && !headers.isEmpty()) { + for (Map.Entry<String, String> header : headers.entrySet()) { + if (StringUtils.hasText(header.getValue())) { + requestBuilder.addHeader(header.getKey(), header.getValue()); + } + } + } + // add accept + if (DispatchConstants.PARSE_DEFAULT.equals(httpProtocol.getParseType()) + || DispatchConstants.PARSE_JSON_PATH.equals(httpProtocol.getParseType())) { + requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + } else if (DispatchConstants.PARSE_XML_PATH.equals(httpProtocol.getParseType())) { + requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE + "," + MediaType.APPLICATION_XML_VALUE); + } else { + requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.ALL_VALUE); + } + + if (httpProtocol.getAuthorization() != null) { + HttpProtocol.Authorization authorization = httpProtocol.getAuthorization(); + if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) { + String value = DispatchConstants.BEARER + " " + authorization.getBearerTokenToken(); + requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value); + } else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) { + if (StringUtils.hasText(authorization.getBasicAuthUsername()) + && StringUtils.hasText(authorization.getBasicAuthPassword())) { + String authStr = authorization.getBasicAuthUsername() + SignConstants.DOUBLE_MARK + authorization.getBasicAuthPassword(); + String encodedAuth = Base64Util.encode(authStr); + requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + SignConstants.BLANK + encodedAuth); + } + } + } + + // if it has payload, would override post params + if (StringUtils.hasLength(httpProtocol.getPayload()) && (HttpMethod.POST.matches(httpMethod) || HttpMethod.PUT.matches(httpMethod))) { + requestBuilder.setEntity(new StringEntity(TimeExpressionUtil.calculate(httpProtocol.getPayload()), StandardCharsets.UTF_8)); + } + + // uri encode + String uri; + if (enableUrlEncoding) { + // if the url contains parameters directly + if (httpProtocol.getUrl().contains("?")) { + String path = httpProtocol.getUrl().substring(0, httpProtocol.getUrl().indexOf("?")); + String query = httpProtocol.getUrl().substring(httpProtocol.getUrl().indexOf("?") + 1); + uri = UriUtils.encodePath(path, "UTF-8") + "?" + UriUtils.encodeQuery(query, "UTF-8"); + } else { + uri = UriUtils.encodePath(httpProtocol.getUrl(), "UTF-8"); + } + } else { + uri = httpProtocol.getUrl(); + } + + // append query params + if (!queryParams.isEmpty()) { + uri += (uri.contains("?") ? "&" : "?") + queryParams; + } + + String finalUri; + if (IpDomainUtil.isHasSchema(httpProtocol.getHost())) { + finalUri = httpProtocol.getHost() + ":" + httpProtocol.getPort() + uri; + } else { + String ipAddressType = IpDomainUtil.checkIpAddressType(httpProtocol.getHost()); + String baseUri = NetworkConstants.IPV6.equals(ipAddressType) + ? String.format("[%s]:%s%s", httpProtocol.getHost(), httpProtocol.getPort(), uri) + : String.format("%s:%s%s", httpProtocol.getHost(), httpProtocol.getPort(), uri); + boolean ssl = Boolean.parseBoolean(httpProtocol.getSsl()); + if (ssl) { + finalUri = NetworkConstants.HTTPS_HEADER + baseUri; + } else { + finalUri = NetworkConstants.HTTP_HEADER + baseUri; + } + } + + try { + requestBuilder.setUri(finalUri); + } catch (IllegalArgumentException e) { + log.warn("Invalid URI with illegal characters: {}. User has disabled URL encoding, not applying any encoding.", finalUri); + throw e; + } + + // custom timeout + int timeout = CollectUtil.getTimeout(httpProtocol.getTimeout(), 0); + if (timeout > 0) { + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(timeout) + .setSocketTimeout(timeout) + .setRedirectsEnabled(true) + .build(); + requestBuilder.setConfig(requestConfig); + } + return requestBuilder.build(); + } + + private boolean checkSuccessInvoke(Metrics metrics, int statusCode) { + List<String> successCodes = metrics.getHttp().getSuccessCodes(); + Set<Integer> successCodeSet = successCodes != null ? successCodes.stream().map(code -> { + try { + return Integer.valueOf(code); + } catch (Exception ignored) { + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toSet()) : defaultSuccessStatusCodes; + if (successCodeSet.isEmpty()) { + successCodeSet = defaultSuccessStatusCodes; + } + return successCodeSet.contains(statusCode); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/SslCertificateCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/SslCertificateCollectImpl.java new file mode 100644 index 0000000..131cd52 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/SslCertificateCollectImpl.java @@ -0,0 +1,201 @@ +/* + * 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.collector.collect.http; + +import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.ConnectException; +import java.net.URL; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Date; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.springframework.util.StringUtils; + +/** + * ssl Certificate + */ +@Slf4j +public class SslCertificateCollectImpl extends AbstractCollect { + + private static final String NAME_SUBJECT = "subject"; + private static final String NAME_EXPIRED = "expired"; + private static final String NAME_START_TIME = "start_time"; + private static final String NAME_START_TIMESTAMP = "start_timestamp"; + private static final String NAME_END_TIME = "end_time"; + private static final String NAME_END_TIMESTAMP = "end_timestamp"; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getHttp() == null) { + throw new IllegalArgumentException("Http/Https collect must has http params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, + Metrics metrics) { + long startTime = System.currentTimeMillis(); + + HttpProtocol httpProtocol = metrics.getHttp(); + String url = httpProtocol.getUrl(); + if (!StringUtils.hasText(url) || !url.startsWith(RIGHT_DASH)) { + httpProtocol.setUrl(StringUtils.hasText(url) ? RIGHT_DASH + url.trim() : RIGHT_DASH); + } + + HttpsURLConnection urlConnection = null; + try { + String uri = ""; + if (IpDomainUtil.isHasSchema(httpProtocol.getHost())) { + uri = httpProtocol.getHost() + ":" + httpProtocol.getPort(); + } else { + uri = "https://" + httpProtocol.getHost() + ":" + httpProtocol.getPort(); + } + urlConnection = (HttpsURLConnection) new URL(uri).openConnection(); + + boolean verifySsl = Boolean.parseBoolean(httpProtocol.getSsl()); + // ignore ssl verify + if (!verifySsl){ + SSLContext ignoreSslContext = createIgnoreVerifySslContext(); + urlConnection.setSSLSocketFactory(ignoreSslContext.getSocketFactory()); + } + + urlConnection.connect(); + Certificate[] certificates = urlConnection.getServerCertificates(); + if (certificates == null || certificates.length == 0) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Ssl certificate does not exist."); + return; + } + + long responseTime = System.currentTimeMillis() - startTime; + for (Certificate certificate : urlConnection.getServerCertificates()) { + X509Certificate x509Certificate = (X509Certificate) certificate; + Date now = new Date(); + Date deadline = x509Certificate.getNotAfter(); + boolean expired = deadline != null && now.after(deadline); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : metrics.getAliasFields()) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Long.toString(responseTime)); + } else if (NAME_SUBJECT.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(x509Certificate.getSubjectX500Principal().getName()); + } else if (NAME_EXPIRED.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Boolean.toString(expired)); + } else if (NAME_START_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(x509Certificate.getNotBefore().toLocaleString()); + } else if (NAME_START_TIMESTAMP.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(String.valueOf(x509Certificate.getNotBefore().getTime())); + } else if (NAME_END_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(x509Certificate.getNotAfter().toLocaleString()); + } else if (NAME_END_TIMESTAMP.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(String.valueOf(x509Certificate.getNotAfter().getTime())); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } catch (SSLPeerUnverifiedException e1) { + String errorMsg = "Ssl certificate does not exist."; + if (e1.getMessage() != null) { + errorMsg = e1.getMessage(); + log.error(errorMsg); + } + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } catch (UnknownHostException e2) { + String errorMsg = CommonUtil.getMessageFromThrowable(e2); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_REACHABLE); + builder.setMsg("unknown host:" + errorMsg); + } catch (InterruptedIOException | ConnectException | SSLException e3) { + String errorMsg = CommonUtil.getMessageFromThrowable(e3); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (IOException e4) { + String errorMsg = CommonUtil.getMessageFromThrowable(e4); + log.info(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error(errorMsg, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_SSL_CERT; + } + + private void validateParams(Metrics metrics) { + + } + + public SSLContext createIgnoreVerifySslContext() throws NoSuchAlgorithmException, KeyManagementException { + SSLContext sc = SSLContext.getInstance("TLS"); + X509TrustManager trustManager = new X509TrustManager() { + @Override + public void checkClientTrusted( + java.security.cert.X509Certificate[] paramArrayOfX509Certificate, + String paramString) { + } + + @Override + public void checkServerTrusted( + java.security.cert.X509Certificate[] paramArrayOfX509Certificate, + String paramString) { + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + sc.init(null, new TrustManager[]{trustManager}, null); + return sc; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/AbstractPrometheusParse.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/AbstractPrometheusParse.java new file mode 100644 index 0000000..2195ac2 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/AbstractPrometheusParse.java @@ -0,0 +1,80 @@ +/* + * 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.collector.collect.http.promethus; + +import java.util.List; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * prometheus parse abstract class + * todo: parse response formats for string and scalar types + */ +public abstract class AbstractPrometheusParse { + + /** + * Downstream node + */ + private AbstractPrometheusParse prometheusParse; + + AbstractPrometheusParse() { + } + + public AbstractPrometheusParse setInstance(AbstractPrometheusParse prometheusParse) { + this.prometheusParse = prometheusParse; + return this; + } + + /** + * checks the Prometheus response type: string, matrix, vector, scalar + * todo:implementation for string and scalar types is missing + * @param responseStr The returned string + * @return boolean indicating the result + */ + abstract Boolean checkType(String responseStr); + + /** + * Parse the prom interface response data + * @param resp The returned data + * @param aliasFields alias fields + * @param http httpProtocol + * @param builder builder + */ + abstract void parse(String resp, List<String> aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder); + + /** + * Processing prom interface response data + * @param resp resp + * @param aliasFields alias fields + * @param http http + * @param builder builder + */ + public void handle(String resp, List<String> aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder) { + if (checkType(resp)) { + parse(resp, aliasFields, http, + builder); + } else { + prometheusParse.handle(resp, aliasFields, http, + builder); + } + } + + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/ParseException.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/ParseException.java new file mode 100644 index 0000000..a571e1c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/ParseException.java @@ -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.collector.collect.http.promethus; + +/** + * Parse Exception + */ +public class ParseException extends RuntimeException { + + public ParseException(String msg) { + super(msg); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusLastParser.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusLastParser.java new file mode 100644 index 0000000..a2b28e6 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusLastParser.java @@ -0,0 +1,44 @@ +/* + * 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.collector.collect.http.promethus; + +import java.util.List; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * Prometheus Last Parser + */ +@Slf4j +@NoArgsConstructor +public class PrometheusLastParser extends AbstractPrometheusParse { + @Override + public Boolean checkType(String responseStr) { + log.error("prometheus response data:{} ,no adaptive parser", responseStr); + return true; + } + + @Override + public void parse(String resp, List<String> aliasFields, HttpProtocol http, CollectRep.MetricsData.Builder builder) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + aliasFields.forEach(aliasField -> valueRowBuilder.addColumn(CommonConstants.NULL_VALUE)); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusMatrixParser.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusMatrixParser.java new file mode 100644 index 0000000..37f80ad --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusMatrixParser.java @@ -0,0 +1,98 @@ +/* + * 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.collector.collect.http.promethus; + +import com.google.gson.JsonElement; +import java.math.BigDecimal; +import java.util.List; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.PromVectorOrMatrix; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.JsonUtil; + +/** + * Processing prometheus returns a response format of type "matrix" + */ +@NoArgsConstructor +public class PrometheusMatrixParser extends AbstractPrometheusParse { + @Override + public Boolean checkType(String responseStr) { + try { + PromVectorOrMatrix promVectorOrMatrix = JsonUtil.fromJson(responseStr, PromVectorOrMatrix.class); + if (promVectorOrMatrix != null && promVectorOrMatrix.getData() != null) { + return DispatchConstants.PARSE_PROM_QL_MATRIX.equals(promVectorOrMatrix.getData().getResultType()); + } + return false; + } catch (Exception e) { + return false; + } + } + + @Override + public void parse(String resp, List<String> aliasFields, HttpProtocol http, CollectRep.MetricsData.Builder builder) { + PromVectorOrMatrix promVectorOrMatrix = JsonUtil.fromJson(resp, PromVectorOrMatrix.class); + if (promVectorOrMatrix == null){ + return; + } + List<PromVectorOrMatrix.Result> result = promVectorOrMatrix.getData().getResult(); + for (PromVectorOrMatrix.Result r : result) { + for (List<Object> value : r.getValues()) { + boolean setTimeFlag = false; + boolean setValueFlag = false; + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String aliasField : aliasFields) { + if (!CollectUtil.assertPromRequireField(aliasField) || CollectUtil.isValueAsLabel(aliasField, r.getMetric())) { + JsonElement jsonElement = r.getMetric().get(aliasField); + if (jsonElement != null) { + valueRowBuilder.addColumn(jsonElement.getAsString()); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } else { + if (CommonConstants.PROM_TIME.equals(aliasField)) { + for (Object o : value) { + if (o instanceof Double time) { + valueRowBuilder.addColumn(String.valueOf(BigDecimal.valueOf(time * 1000))); + setTimeFlag = true; + } + } + if (!setTimeFlag) { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } else { + for (Object o : value) { + if (o instanceof String str) { + valueRowBuilder.addColumn(str); + setValueFlag = true; + } + } + if (!setValueFlag) { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusParseCreator.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusParseCreator.java new file mode 100644 index 0000000..72b83e3 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusParseCreator.java @@ -0,0 +1,44 @@ +/* + * 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.collector.collect.http.promethus; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +/** + * prometheus parse creater + */ +@Slf4j +@Component +public class PrometheusParseCreator implements InitializingBean { + private static final AbstractPrometheusParse PROMETHEUS_PARSE = new PrometheusVectorParser(); + + private static void create() { + PROMETHEUS_PARSE.setInstance(new PrometheusMatrixParser().setInstance(new PrometheusLastParser())); + } + + public static AbstractPrometheusParse getPrometheusParse(){ + return PROMETHEUS_PARSE; + } + + @Override + public void afterPropertiesSet() throws Exception { + create(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusVectorParser.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusVectorParser.java new file mode 100644 index 0000000..3045c0a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusVectorParser.java @@ -0,0 +1,96 @@ +/* + * 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.collector.collect.http.promethus; + +import com.google.gson.JsonElement; +import java.math.BigDecimal; +import java.util.List; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.PromVectorOrMatrix; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.JsonUtil; + +/** + * Processing prometheus returns a response format of type "vector" + */ +@NoArgsConstructor +public class PrometheusVectorParser extends AbstractPrometheusParse { + @Override + public Boolean checkType(String responseStr) { + try { + PromVectorOrMatrix promVectorOrMatrix = JsonUtil.fromJson(responseStr, PromVectorOrMatrix.class); + if (promVectorOrMatrix != null && promVectorOrMatrix.getData() != null) { + return DispatchConstants.PARSE_PROM_QL_VECTOR.equals(promVectorOrMatrix.getData().getResultType()); + } + return false; + } catch (Exception e) { + return false; + } + } + + @Override + public void parse(String resp, List<String> aliasFields, HttpProtocol http, CollectRep.MetricsData.Builder builder) { + boolean setTimeFlag = false; + boolean setValueFlag = false; + PromVectorOrMatrix promVectorOrMatrix = JsonUtil.fromJson(resp, PromVectorOrMatrix.class); + if (promVectorOrMatrix == null){ + return; + } + List<PromVectorOrMatrix.Result> result = promVectorOrMatrix.getData().getResult(); + for (PromVectorOrMatrix.Result r : result) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String aliasField : aliasFields) { + if (!CollectUtil.assertPromRequireField(aliasField) || CollectUtil.isValueAsLabel(aliasField, r.getMetric())) { + JsonElement jsonElement = r.getMetric().get(aliasField); + if (jsonElement != null) { + valueRowBuilder.addColumn(jsonElement.getAsString()); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } else { + if (CommonConstants.PROM_TIME.equals(aliasField)) { + for (Object o : r.getValue()) { + if (o instanceof Double time) { + valueRowBuilder.addColumn(String.valueOf(BigDecimal.valueOf(time * 1000))); + setTimeFlag = true; + } + } + if (!setTimeFlag) { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } else { + for (Object o : r.getValue()) { + if (o instanceof String str) { + valueRowBuilder.addColumn(str); + setValueFlag = true; + } + } + if (!setValueFlag) { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/icmp/IcmpCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/icmp/IcmpCollectImpl.java new file mode 100644 index 0000000..02a2129 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/icmp/IcmpCollectImpl.java @@ -0,0 +1,100 @@ +/* + * 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.collector.collect.icmp; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.IcmpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * icmp ping collect + */ +@Slf4j +public class IcmpCollectImpl extends AbstractCollect { + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getIcmp() == null) { + throw new IllegalArgumentException("ICMP collect must has icmp params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + + IcmpProtocol icmp = metrics.getIcmp(); + // The default timeout is 6000 milliseconds + int timeout = 6000; + try { + timeout = Integer.parseInt(icmp.getTimeout()); + } catch (Exception e) { + log.warn(e.getMessage()); + } + try { + // todo requires Java JVM with root permissions to use ICMP, otherwise check if telnet is available on peer server's port 7 + // todo requires configuring Java JVM with root permissions to use ICMP, otherwise check if telnet is available on the peer's port 7 + // todo https://stackoverflow.com/questions/11506321/how-to-ping-an-ip-address + boolean status = InetAddress.getByName(icmp.getHost()).isReachable(timeout); + long responseTime = System.currentTimeMillis() - startTime; + if (!status) { + builder.setCode(CollectRep.Code.UN_REACHABLE); + builder.setMsg("Un Reachable, Timeout " + timeout + "ms"); + return; + } + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : metrics.getAliasFields()) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Long.toString(responseTime)); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + builder.addValueRow(valueRowBuilder.build()); + } catch (UnknownHostException unknownHostException) { + String errorMsg = CommonUtil.getMessageFromThrowable(unknownHostException); + builder.setCode(CollectRep.Code.UN_REACHABLE); + builder.setMsg("UnknownHost " + errorMsg); + } catch (IOException ioException) { + String errorMsg = CommonUtil.getMessageFromThrowable(ioException); + builder.setCode(CollectRep.Code.UN_REACHABLE); + builder.setMsg("IOException " + errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error(errorMsg, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_ICMP; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/imap/ImapCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/imap/ImapCollectImpl.java new file mode 100644 index 0000000..1649809 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/imap/ImapCollectImpl.java @@ -0,0 +1,169 @@ +/* + * 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.collector.collect.imap; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.net.imap.IMAPClient; +import org.apache.commons.net.imap.IMAPSClient; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.ImapProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.springframework.util.Assert; + +/** + * imap collect + */ +@Slf4j +public class ImapCollectImpl extends AbstractCollect { + + private static final String UTF_7_X = "X-MODIFIED-UTF-7"; + private static final String STATUS = "STATUS"; + private static final String STATUS_COMMAND = "(MESSAGES RECENT UNSEEN)"; + private static final String MESSAGES = "MESSAGES"; + private static final String RECENT = "RECENT"; + private static final String UNSEEN = "UNSEEN"; + private static final String RESPONSETIME = "responseTime"; + private static final String totalMessageCount = "TotalMessageCount"; + private static final String recentMessageCount = "RecentMessageCount"; + private static final String unseenMessageCount = "UnseenMessageCount"; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + ImapProtocol imapProtocol = metrics.getImap(); + Assert.notNull(metrics, "IMAP collect must has Imap params"); + Assert.notNull(metrics.getImap(), "IMAP collect must has Imap params"); + Assert.hasText(imapProtocol.getHost(), "IMAP host is required"); + Assert.hasText(imapProtocol.getPort(), "IMAP port is required"); + Assert.hasText(imapProtocol.getEmail(), "IMAP email is required"); + Assert.hasText(imapProtocol.getAuthorize(), "IMAP authorize code is required"); + Assert.hasText(imapProtocol.getFolderName(), "IMAP folder name is required"); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + ImapProtocol imapProtocol = metrics.getImap(); + IMAPClient imapClient = null; + boolean ssl = Boolean.parseBoolean(imapProtocol.getSsl()); + + try { + imapClient = createImapClient(imapProtocol, ssl); + // if Connected, then collect metrics + if (imapClient.isConnected()) { + long responseTime = System.currentTimeMillis() - startTime; + String folderName = imapProtocol.getFolderName(); + collectImapMetrics(builder, imapClient, metrics.getAliasFields(), folderName, responseTime); + } else { + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer connect failed,Timeout " + imapProtocol.getTimeout() + "ms"); + } + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (imapClient != null) { + try { + imapClient.logout(); + imapClient.disconnect(); + } catch (IOException e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_IMAP; + } + + private IMAPClient createImapClient(ImapProtocol imapProtocol, boolean ssl) throws Exception { + IMAPClient imapClient = null; + // determine whether to use SSL-encrypted connections + imapClient = new IMAPSClient(true); + if (!ssl) { + imapClient = new IMAPClient(); + } + // set timeout + int timeout = Integer.parseInt(imapProtocol.getTimeout()); + if (timeout > 0) { + imapClient.setConnectTimeout(timeout); + } + //set Charset + imapClient.setCharset(StandardCharsets.US_ASCII); + // connect to the IMAP server + String host = imapProtocol.getHost(); + int port = Integer.parseInt(imapProtocol.getPort()); + imapClient.connect(host, port); + // validate credentials + String email = imapProtocol.getEmail(); + String authorize = imapProtocol.getAuthorize(); + boolean isAuthenticated = imapClient.login(email, authorize); + if (!isAuthenticated) { + throw new Exception("IMAP client authentication failed"); + } + return imapClient; + + } + + private void collectImapMetrics(CollectRep.MetricsData.Builder builder, IMAPClient imapClient, List<String> aliasFields, + String folderName, long responseTime) throws Exception { + Map<String, String> resultsMap = new HashMap<>(); + resultsMap.put(RESPONSETIME, String.valueOf(responseTime)); + imapClient.sendCommand(STATUS + " \"" + CollectUtil.stringEncodeUtf7String(folderName, UTF_7_X) + "\" " + STATUS_COMMAND); + String[] response = imapClient.getReplyString().split("\\s+|\\(|\\)"); + for (int i = 0; i < response.length; i++) { + switch (response[i]) { + case MESSAGES: + resultsMap.put(folderName + totalMessageCount, response[i + 1]); + break; + case RECENT: + resultsMap.put(folderName + recentMessageCount, response[i + 1]); + break; + case UNSEEN: + resultsMap.put(folderName + unseenMessageCount, response[i + 1]); + break; + default: + break; + } + } + + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String field : aliasFields) { + String fieldValue = resultsMap.get(field); + valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } + builder.addValueRow(valueRowBuilder.build()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/IpmiCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/IpmiCollectImpl.java new file mode 100644 index 0000000..fc7374b --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/IpmiCollectImpl.java @@ -0,0 +1,114 @@ +/* + * 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.collector.collect.ipmi2; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection; +import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier; +import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache; +import org.apache.hertzbeat.collector.collect.ipmi2.cache.IpmiConnect; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiClient; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiConnection; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiHandlerManager; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.IpmiProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.util.Optional; + +/** + * Ipmi collect implementation + */ +@Slf4j +public class IpmiCollectImpl extends AbstractCollect { + + private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance(); + + private final IpmiHandlerManager ipmiHandlerManager; + + public IpmiCollectImpl() { + ipmiHandlerManager = new IpmiHandlerManager(); + } + + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getIpmi() == null) { + throw new IllegalArgumentException("Ipmi collect must has ipmi params"); + } + IpmiProtocol ipmiProtocol = metrics.getIpmi(); + Assert.hasText(ipmiProtocol.getHost(), "Ipmi Protocol host is required."); + Assert.hasText(ipmiProtocol.getPort(), "Ipmi Protocol port is required."); + Assert.hasText(ipmiProtocol.getUsername(), "Ipmi Protocol username is required."); + Assert.hasText(ipmiProtocol.getPassword(), "Ipmi Protocol password is required."); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + IpmiConnection connection = null; + try { + connection = getIpmiConnection(metrics.getIpmi()); + } catch (Exception e) { + log.error("Ipmi session create error: {}", e.getMessage()); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(e.getMessage()); + return; + } + try { + connection.getResource(builder, metrics); + } catch (IOException e) { + log.error("Get Ipmi {} detail resource error: {}", metrics.getName(), e.getMessage()); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_IPMI; + } + + + private IpmiConnection getIpmiConnection(IpmiProtocol ipmiProtocol) throws Exception { + CacheIdentifier identifier = CacheIdentifier.builder() + .ip(ipmiProtocol.getHost()) + .port(ipmiProtocol.getPort()) + .username(ipmiProtocol.getUsername()) + .password(ipmiProtocol.getPassword()) + .build(); + IpmiConnection connection = null; + Optional<AbstractConnection<?>> cacheOption = connectionCommonCache.getCache(identifier, true); + if (cacheOption.isPresent()) { + IpmiConnect ipmiConnect = (IpmiConnect) cacheOption.get(); + connection = ipmiConnect.getConnection(); + if (connection == null || !connection.isActive()) { + connection = null; + connectionCommonCache.removeCache(identifier); + } + } + if (connection != null) { + return connection; + } + IpmiClient ipmiClient = IpmiClient.create(ipmiProtocol); + connection = ipmiClient.connect(); + connectionCommonCache.addCache(identifier, new IpmiConnect(connection)); + return connection; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/cache/IpmiConnect.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/cache/IpmiConnect.java new file mode 100644 index 0000000..deba8dd --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/cache/IpmiConnect.java @@ -0,0 +1,47 @@ +/* + * 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.collector.collect.ipmi2.cache; + + +import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiConnection; + +/** + * ipmi connect session + */ +public class IpmiConnect extends AbstractConnection<IpmiConnection> { + + private final IpmiConnection ipmiConnection; + + public IpmiConnect(IpmiConnection ipmiConnection) { + this.ipmiConnection = ipmiConnection; + } + + @Override + public IpmiConnection getConnection() { + return ipmiConnection; + } + + @Override + public void closeConnection() throws Exception { + if (ipmiConnection != null) { + ipmiConnection.close(); + } + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiClient.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiClient.java new file mode 100644 index 0000000..2a9ac26 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiClient.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.client; + +import java.io.IOException; +import java.security.SecureRandom; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RakpMessage1; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RakpMessage2; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RakpMessage3; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RakpMessage4; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RmcpPlusOpenSessionRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RmcpPlusOpenSessionResponse; +import org.apache.hertzbeat.common.entity.job.protocol.IpmiProtocol; + +/** + * IpmiClient used to connect to a remote Ipmi server + */ +public class IpmiClient { + + UdpConnection connection; + private final String host; + private final Integer port; + private final String username; + private final String password; + + public IpmiClient(String host, Integer port, String username, String password) throws IOException { + this.host = host; + this.port = port; + this.username = username; + this.password = password; + connection = new UdpConnection(host, port); + } + + public static IpmiClient create(IpmiProtocol ipmiProtocol) throws IOException { + return new IpmiClient(ipmiProtocol.getHost(), Integer.parseInt(ipmiProtocol.getPort()), + ipmiProtocol.getUsername(), ipmiProtocol.getPassword()); + } + + public IpmiConnection connect() throws IOException { + IpmiSession session = newSession(username, password); + + RmcpPlusOpenSessionResponse rmcpPlusOpenSessionResponse = connection.get(session, new RmcpPlusOpenSessionRequest(), RmcpPlusOpenSessionResponse.class); + session.setSystemSessionId(rmcpPlusOpenSessionResponse.systemSessionId); + + session.generateConsoleRandomNumber(); + RakpMessage2 rakpMessage2 = connection.get(session, new RakpMessage1(), RakpMessage2.class); + session.setSystemRandomNumber(rakpMessage2.systemRandom); + session.setSystemGuid(rakpMessage2.systemGuid); + + + session.generateSik(); + session.setK1(session.generateK(1)); + session.setK2(session.generateK(2)); + connection.get(session, new RakpMessage3(), RakpMessage4.class); + + session.setConnected(true); + return new IpmiConnection(session, connection); + } + + public IpmiSession newSession(String username, String password) { + SecureRandom random = new SecureRandom(); + IpmiSession session = new IpmiSession(random.nextInt()); + session.setUserName(username); + session.setPassword(password); + return session; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiConnection.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiConnection.java new file mode 100644 index 0000000..a366d63 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiConnection.java @@ -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.collector.collect.ipmi2.client; + +import java.io.IOException; +import org.apache.hertzbeat.collector.collect.ipmi2.client.handler.IpmiHandler; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.messaging.CloseSessionRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.messaging.CloseSessionResponse; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * IpmiConnection used for sending ipmi request + */ +public class IpmiConnection implements AutoCloseable { + + IpmiSession session; + + UdpConnection udpConnection; + + IpmiHandlerManager handlerManager = new IpmiHandlerManager(); + + private volatile boolean active = true; + + IpmiConnection(IpmiSession session, UdpConnection udpConnection) { + this.session = session; + this.udpConnection = udpConnection; + } + + public void getResource(CollectRep.MetricsData.Builder builder, Metrics metrics) throws IOException { + IpmiHandler handler = handlerManager.getHandler(metrics.getName()); + if (handler == null) { + throw new RuntimeException("no handler for " + metrics.getIpmi().getType()); + } + handler.handler(session, udpConnection, builder, metrics); + } + + + @Override + public void close() throws IOException { + udpConnection.get(session, new CloseSessionRequest(session.getSystemSessionId()), CloseSessionResponse.class); + udpConnection.close(); + session = null; + active = false; + } + + public boolean isActive() { + return this.active; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiEncoderDecoder.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiEncoderDecoder.java new file mode 100644 index 0000000..f0d52d4 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiEncoderDecoder.java @@ -0,0 +1,41 @@ +/* + * 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.collector.collect.ipmi2.client; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.rmcp.RmcpPacket; + +/** + * IPMI encoder and decoder + */ +public class IpmiEncoderDecoder { + + public static ByteBuffer encode(IpmiPacketContext context, RmcpPacket rmcpPacket) { + int length = rmcpPacket.getWireLength(context.getIpmiSession()); + ByteBuffer buffer = ByteBuffer.allocate(length); + rmcpPacket.toWire(context, buffer); + buffer.flip(); + return buffer; + } + + public static RmcpPacket decode(IpmiPacketContext context, ByteBuffer buffer) { + RmcpPacket rmcpPacket = new RmcpPacket(); + rmcpPacket.fromWire(context, buffer); + return rmcpPacket; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiHandlerManager.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiHandlerManager.java new file mode 100644 index 0000000..7e68371 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiHandlerManager.java @@ -0,0 +1,50 @@ +/* + * 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.collector.collect.ipmi2.client; + + +import java.util.HashMap; +import java.util.Map; +import org.apache.hertzbeat.collector.collect.ipmi2.client.handler.ChassisHandler; +import org.apache.hertzbeat.collector.collect.ipmi2.client.handler.IpmiHandler; +import org.apache.hertzbeat.collector.collect.ipmi2.client.handler.SensorHandler; + +/** + * Manger for IpmiHandler + */ +public class IpmiHandlerManager { + + Map<String, IpmiHandler> handlerMap = new HashMap<>(); + + public IpmiHandlerManager() { + registerHandler("Chassis", new ChassisHandler()); + registerHandler("Sensor", new SensorHandler()); + } + + public void registerHandler(String metricName, IpmiHandler handler) { + handlerMap.put(metricName, handler); + } + + public IpmiHandler getHandler(String metricName) { + if (!handlerMap.containsKey(metricName)) { + return null; + } + return handlerMap.get(metricName); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiPacketContext.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiPacketContext.java new file mode 100644 index 0000000..6f3e262 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiPacketContext.java @@ -0,0 +1,25 @@ +/* + * 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.collector.collect.ipmi2.client; + +/** + * Ipmi packet context + */ +public interface IpmiPacketContext { + IpmiSession getIpmiSession(); +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiSession.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiSession.java new file mode 100644 index 0000000..f62ef00 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/IpmiSession.java @@ -0,0 +1,133 @@ +/* + * 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.collector.collect.ipmi2.client; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.Data; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.AbstractWireable; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.AbstractSessionIpmiPayload; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiAuthenticationCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiConfidentialityCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiIntegrityCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.authentication.IpmiAuthentication; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteOrderUtils; + + +/** + * Ipmi session + */ +@Data +public class IpmiSession implements IpmiPacketContext { + + private final int consoleSessionId; + + private int systemSessionId; + + private byte[] consoleRandomNumber; + + private byte[] systemRandomNumber; + + private byte[] systemGuid; + + private byte[] sik; + + private byte[] k1; + + private byte[] k2; + + private String userName; + + private String password; + + private boolean isConnected = false; + + private AtomicInteger authenticatedSequenceNumber = new AtomicInteger(1); + + private AtomicInteger unauthenticatedSequenceNumber = new AtomicInteger(1); + + private IpmiAuthenticationCode authenticationAlgorithm = IpmiAuthenticationCode.RAKP_HMAC_SHA1; + private IpmiConfidentialityCode confidentialityAlgorithm = IpmiConfidentialityCode.AES_CBC_128; + private IpmiIntegrityCode integrityAlgorithm = IpmiIntegrityCode.HMAC_SHA1_96; + + private AbstractSessionIpmiPayload.MaximumPrivilegeLevel maximumPrivilegeLevel = AbstractSessionIpmiPayload.MaximumPrivilegeLevel.ADMINISTRATOR; + + public IpmiSession(int consoleSessionId) { + this.consoleSessionId = consoleSessionId; + } + + public void generateSik() { + int length = 34 + ((userName == null) ? 0 : userName.length()); + ByteBuffer buffer = ByteBuffer.allocate(length); + ByteOrderUtils.writeBytes(buffer, consoleRandomNumber); + ByteOrderUtils.writeBytes(buffer, systemRandomNumber); + byte t = AbstractWireable.setBits((byte) 0, 4, 0x1, 1); + AbstractSessionIpmiPayload.MaximumPrivilegeLevel maximumPrivilegeLevel = this.getMaximumPrivilegeLevel(); + t = AbstractWireable.setBits(t, 0, AbstractSessionIpmiPayload.MaximumPrivilegeLevel.MASK, maximumPrivilegeLevel.getCode()); + buffer.put(t); + if (userName != null) { + byte[] usernameBytes = userName.getBytes(StandardCharsets.US_ASCII); + buffer.put((byte) usernameBytes.length); + buffer.put(usernameBytes); + } else { + buffer.put((byte) 0); + } + IpmiAuthenticationCode authenticationCode = authenticationAlgorithm; + IpmiAuthentication authentication = authenticationCode.newIpmiAuthentication(); + if (authentication == null) { + throw new UnsupportedOperationException("Unsupported authentication code: " + authenticationCode); + } + try { + authentication.setKey(password.getBytes(StandardCharsets.US_ASCII)); + authentication.setData(buffer.array()); + sik = authentication.getHash(); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } + } + + public byte[] generateK(int n) { + byte[] data = new byte[20]; + Arrays.fill(data, (byte) n); + IpmiAuthentication authentication = authenticationAlgorithm.newIpmiAuthentication(); + if (authentication == null) { + throw new UnsupportedOperationException("Unsupported authentication code: " + authenticationAlgorithm); + } + try { + authentication.setKey(sik); + authentication.setData(data); + return authentication.getHash(); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } + } + + public void generateConsoleRandomNumber() { + SecureRandom random = new SecureRandom(); + consoleRandomNumber = random.generateSeed(16); + } + + @Override + public IpmiSession getIpmiSession() { + return this; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/UdpConnection.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/UdpConnection.java new file mode 100644 index 0000000..27c6b9f --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/UdpConnection.java @@ -0,0 +1,81 @@ +/* + * 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.collector.collect.ipmi2.client; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.Ipmi20Ipv4SessionWrapper; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.IpmiPayload; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.rmcp.RmcpPacket; + +/** + * Udp connection for ipmi + */ +public class UdpConnection { + final String host; + final int port; + + final SocketAddress address; + + DatagramChannel channel; + + ByteBuffer receiveBuffer = ByteBuffer.allocate(1024); + + + public UdpConnection(String host, int port) throws IOException { + this.host = host; + this.port = port; + this.address = new InetSocketAddress(host, port); + channel = DatagramChannel.open(); + } + + private int send(IpmiPacketContext context, IpmiPayload payload) throws IOException { + Ipmi20Ipv4SessionWrapper wrapper = new Ipmi20Ipv4SessionWrapper(); + wrapper.setIpmiPayload(payload); + if (context.getIpmiSession().isConnected()) { + wrapper.setIpmiSessionId(context.getIpmiSession().getSystemSessionId()); + wrapper.setIpmiSessionSequenceNumber(context.getIpmiSession().getAuthenticatedSequenceNumber().getAndIncrement()); + } + RmcpPacket rmcpPacket = new RmcpPacket(); + rmcpPacket.withData(wrapper); + ByteBuffer sendBuffer = IpmiEncoderDecoder.encode(context, rmcpPacket); + return channel.send(sendBuffer, address); + } + + private <T extends IpmiPayload> T receive(IpmiPacketContext context, Class<T> clazz) throws IOException { + receiveBuffer.clear(); + channel.receive(receiveBuffer); + receiveBuffer.flip(); + RmcpPacket rmcpPacket = IpmiEncoderDecoder.decode(context, receiveBuffer); + return rmcpPacket.getEncapsulated(clazz); + } + + public <T extends IpmiPayload> T get(IpmiPacketContext context, IpmiPayload payload, Class<T> clazz) throws IOException { + send(context, payload); + return receive(context, clazz); + } + + public void close() throws IOException { + channel.close(); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/handler/ChassisHandler.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/handler/ChassisHandler.java new file mode 100644 index 0000000..b9e6806 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/handler/ChassisHandler.java @@ -0,0 +1,66 @@ +/* + * 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.collector.collect.ipmi2.client.handler; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; +import org.apache.hertzbeat.collector.collect.ipmi2.client.UdpConnection; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.chassis.GetChassisStatusRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.chassis.GetChassisStatusResponse; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * ChassisHandler + */ +public class ChassisHandler implements IpmiHandler { + + Map<String, String> parseValue = new HashMap<>(); + + @Override + public void handler(IpmiSession session, UdpConnection connection, CollectRep.MetricsData.Builder builder, Metrics metrics) throws IOException { + GetChassisStatusResponse getChassisStatusResponse = connection.get(session, new GetChassisStatusRequest(), GetChassisStatusResponse.class); + parseFieldToMap(getChassisStatusResponse); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (Metrics.Field field : metrics.getFields()) { + if (!parseValue.containsKey(field.getField())) { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + continue; + } + valueRowBuilder.addColumn(parseValue.get(field.getField())); + } + builder.addValueRow(valueRowBuilder.build()); + } + + public void parseFieldToMap(GetChassisStatusResponse getChassisStatusResponse) { + parseValue.put("system_power", getChassisStatusResponse.isPowerOn ? "on" : "off"); + parseValue.put("power_overload", getChassisStatusResponse.isPowerOverload ? "true" : "false"); + parseValue.put("power_interlock", getChassisStatusResponse.isPowerInterlock ? "true" : "false"); + parseValue.put("power_fault", getChassisStatusResponse.isPowerFault ? "true" : "false"); + parseValue.put("power_control_fault", getChassisStatusResponse.isPowerControlFault ? "true" : "false"); + parseValue.put("power_restore_policy", getChassisStatusResponse.powerRestorePolicy); + parseValue.put("last_power_event", getChassisStatusResponse.lastPowerEvent); + parseValue.put("fan_fault", getChassisStatusResponse.isFanFault ? "true" : "false"); + parseValue.put("drive_fault", getChassisStatusResponse.isDriveFault ? "true" : "false"); + parseValue.put("front_panel_lockout_active", getChassisStatusResponse.isFrontPanelLockoutActive ? "true" : "false"); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/handler/IpmiHandler.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/handler/IpmiHandler.java new file mode 100644 index 0000000..6620d15 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/handler/IpmiHandler.java @@ -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. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.client.handler; + +import java.io.IOException; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; +import org.apache.hertzbeat.collector.collect.ipmi2.client.UdpConnection; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * IpmiHandler interface + */ +public interface IpmiHandler { + void handler(IpmiSession session, UdpConnection connection, CollectRep.MetricsData.Builder builder, Metrics metrics) throws IOException; + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/handler/SensorHandler.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/handler/SensorHandler.java new file mode 100644 index 0000000..7ec9315 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/client/handler/SensorHandler.java @@ -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.collector.collect.ipmi2.client.handler; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; +import org.apache.hertzbeat.collector.collect.ipmi2.client.UdpConnection; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.GetSdrRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.GetSdrResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.GetSensorReadingRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.GetSensorReadingResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.ReserveSdrRepositoryRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.ReserveSdrRepositoryResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.code.IpmiReadingTypeCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * SensorHandler + */ +@Slf4j +public class SensorHandler implements IpmiHandler { + + @Override + public void handler(IpmiSession session, UdpConnection connection, CollectRep.MetricsData.Builder builder, Metrics metrics) throws IOException { + ReserveSdrRepositoryResponse response = connection.get(session, new ReserveSdrRepositoryRequest(), ReserveSdrRepositoryResponse.class); + int reserveId = response.reserveId; + int recordId = GetSdrRequest.RECORD_ID_START; + while (true) { + if (recordId == 0xFFFF) { + break; + } + GetSdrRequest headRequest = new GetSdrRequest(reserveId, recordId, (byte) 0, GetSdrRequest.HEADER_LENGTH); + GetSdrResponse getSdrHeadResponse = connection.get(session, headRequest, GetSdrResponse.class); + if (getSdrHeadResponse.recordType != 0x01) { + recordId = getSdrHeadResponse.nextRecordId; + continue; + } + Map<String, String> parseValue = new HashMap<>(); + GetSdrRequest request = new GetSdrRequest(reserveId, recordId, (byte) 0, (byte) (getSdrHeadResponse.recordLength + 5)); + recordId = getSdrHeadResponse.nextRecordId; + GetSdrResponse getSdrBodyResponse = connection.get(session, request, GetSdrResponse.class); + if (isReadable(getSdrBodyResponse)) { + byte sensorNum = getSdrBodyResponse.sensorNumber; + GetSensorReadingRequest readingRequest = new GetSensorReadingRequest(sensorNum); + readingRequest.setRqLun(getSdrBodyResponse.sensorOwnerLun); + try { + GetSensorReadingResponse getSensorReadingResponse = connection.get(session, readingRequest, GetSensorReadingResponse.class); + double sensorReading = calcSensorValue(getSdrBodyResponse, getSensorReadingResponse.sensorReading); + parseValue.put("sensor_reading", String.format("%.3f", sensorReading) + " " + getSdrBodyResponse.unitTypeCode.getDescription()); + } catch (Exception e) { + log.error("get sensor reading error", e); + } + } + parseValue.put("sensor_id", getSdrBodyResponse.sensorIdString); + parseValue.put("entity_id", getSdrBodyResponse.entityIdCode.getDescription()); + parseValue.put("sensor_type", getSdrBodyResponse.sensorTypeCode.getDescription()); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (Metrics.Field field : metrics.getFields()) { + if (!parseValue.containsKey(field.getField())) { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + continue; + } + valueRowBuilder.addColumn(parseValue.get(field.getField())); + } + builder.addValueRow(valueRowBuilder.build()); + } + } + + public boolean isReadable(GetSdrResponse response) { + if (response.recordType != 0x01) { + return false; + } + if (response.readingTypeCode != IpmiReadingTypeCode.Threshold) { + return false; + } + if (response.analogDataFormat == 0x03) { + return false; + } + return true; + } + + public double calcSensorValue(GetSdrResponse response, int raw) { + switch (response.analogDataFormat) { + case 0x00 -> raw = raw & 0xFF; + case 0x01 -> { + int signbit = raw & 0x80; + if (signbit != 0) { + raw = ~raw & 0x7F; + raw = -raw; + } + } + case 0x02 -> raw = ByteConvertUtils.getBitsAsSigned(raw, 8); + default -> { + return 0.0; + } + } + double value = (response.m * raw + response.b * Math.pow(10, response.k1)) * Math.pow(10, response.k2); + switch (response.linear) { + case 0x01 -> value = Math.log(value); + case 0x02 -> value = Math.log10(value); + case 0x03 -> value = Math.log(value) / Math.log(2); + case 0x04 -> value = Math.exp(value); + case 0x05 -> value = Math.pow(10, value); + case 0x06 -> value = Math.pow(2, value); + case 0x07 -> value = Math.pow(value, -1.0); + case 0x08 -> value = Math.pow(value, 2.0); + case 0x09 -> value = Math.pow(value, 3.0); + case 0x0A -> value = Math.sqrt(value); + case 0x0B -> value = Math.cbrt(value); + default -> { + } + } + return value; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/common/AbstractWireable.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/common/AbstractWireable.java new file mode 100644 index 0000000..2504dd0 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/common/AbstractWireable.java @@ -0,0 +1,59 @@ +/* + * 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.collector.collect.ipmi2.protocol.common; + +import java.nio.ByteBuffer; + +/** + * Abstract wireable + */ +public abstract class AbstractWireable implements Wireable { + + public static byte setBits(byte b, int byteIndex, int byteMask, int byteValue) { + byteMask <<= byteIndex; + return (byte) ((b & ~byteMask) | ((byteValue << byteIndex) & byteMask)); + } + + public static byte setBits(byte b, int byteMask, int byteValue) { + return setBits(b, 0, byteMask, byteValue); + } + + public static byte setBits(byte b, int byteValue) { + return setBits(b, 0, 0xff, byteValue); + } + + public static byte getBits(byte b, int byteIndex, int byteMask) { + return (byte) ((b >> byteIndex) & byteMask); + } + + public static byte getBits(byte b, int byteMask) { + return getBits(b, 0, byteMask); + } + + public static void ignoreBytes(ByteBuffer buffer, int length) { + for (int i = 0; i < length; i++) { + buffer.get(); + } + } + + public static void reservedBytes(ByteBuffer buffer, int length) { + for (int i = 0; i < length; i++) { + buffer.put((byte) 0); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/common/IpmiCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/common/IpmiCode.java new file mode 100644 index 0000000..042c6c1 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/common/IpmiCode.java @@ -0,0 +1,61 @@ +/* + * 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.collector.collect.ipmi2.protocol.common; + +import java.nio.ByteBuffer; +import com.google.common.primitives.UnsignedBytes; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * IPMI code + */ +public class IpmiCode { + + /** + * IPMI code + */ + public interface Code { + byte getCode(); + + default String getDescription() { + return "No description"; + } + } + + public static <T extends Enum<T> & IpmiCode.Code> T fromByte(Class<T> type, byte code) { + for (T value : type.getEnumConstants()) { + if (value.getCode() == code) { + return value; + } + } + throw new IllegalArgumentException("Unknown " + type.getSimpleName() + " code 0x" + UnsignedBytes.toString(code, 16)); + } + + public static <T extends Enum<T> & IpmiCode.Code> T fromInt(Class<T> type, int code) { + return fromByte(type, ByteConvertUtils.checkCastByte(code)); + } + + public static <T extends Enum<T> & IpmiCode.Code> T fromBuffer(Class<T> type, ByteBuffer buffer) { + return fromByte(type, buffer.get()); + } + + public static <T extends Enum<T> & IpmiCode.Code> T fromBufferWithMask(Class<T> type, ByteBuffer buffer, int mask) { + return fromByte(type, (byte) (buffer.get() & mask)); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/common/IpmiEncapsulation.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/common/IpmiEncapsulation.java new file mode 100644 index 0000000..6c0835f --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/common/IpmiEncapsulation.java @@ -0,0 +1,42 @@ +/* + * 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.collector.collect.ipmi2.protocol.common; + +/** + * Ipmi encapsulation used to get encapsulated object + */ +public class IpmiEncapsulation { + + /** + * Encapsulation interface + */ + public interface Encapsulation { + <T> T getEncapsulated(Class<T> type); + } + + public static <T> T getEncapsulated(Class<T> type, Object value) { + if (type.isInstance(value)) { + return type.cast(value); + } + if (value instanceof Encapsulation) { + return ((Encapsulation) value).getEncapsulated(type); + } + return null; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/common/Wireable.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/common/Wireable.java new file mode 100644 index 0000000..d775ee7 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/common/Wireable.java @@ -0,0 +1,36 @@ +/* + * 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.collector.collect.ipmi2.protocol.common; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; + + +/** + * Wireable interface to serialize and deserialize + */ +public interface Wireable { + + int getWireLength(IpmiPacketContext context); + + void toWire(IpmiPacketContext context, ByteBuffer buffer); + + void fromWire(IpmiPacketContext context, ByteBuffer buffer); + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/AbstractIpmiSessionWrapper.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/AbstractIpmiSessionWrapper.java new file mode 100644 index 0000000..e9f106d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/AbstractIpmiSessionWrapper.java @@ -0,0 +1,112 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.AbstractWireable; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiEncapsulation; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.IpmiPayload; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.IpmiPayloadType; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RakpMessage1; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RakpMessage2; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RakpMessage3; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RakpMessage4; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RmcpPlusOpenSessionRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RmcpPlusOpenSessionResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.rmcp.RmcpMessageClass; + +/** + * Abstract Ipmi Session Wrapper + */ +public abstract class AbstractIpmiSessionWrapper extends AbstractWireable implements IpmiSessionWrapper { + private int sessionId = 0; + private int sessionSequenceNumber = 0; + private IpmiPayload payload; + + @Override + public int getIpmiSessionId() { + return sessionId; + } + + @Override + public void setIpmiSessionId(int ipmiSessionId) { + this.sessionId = ipmiSessionId; + } + + @Override + public int getIpmiSessionSequenceNumber() { + return sessionSequenceNumber; + } + + @Override + public void setIpmiSessionSequenceNumber(int ipmiSessionSequenceNumber) { + this.sessionSequenceNumber = ipmiSessionSequenceNumber; + } + + @Override + public IpmiPayload getIpmiPayload() { + return payload; + } + + @Override + public void setIpmiPayload(IpmiPayload ipmiPayload) { + this.payload = ipmiPayload; + } + + @Override + public <T> T getEncapsulated(Class<T> type) { + if (type.isInstance(this)) { + return type.cast(this); + } + return IpmiEncapsulation.getEncapsulated(type, getIpmiPayload()); + } + + @Override + public RmcpMessageClass getMessageClass() { + return RmcpMessageClass.IPMI; + } + + public static IpmiPayload newIpmiPayload(ByteBuffer buffer, IpmiPayloadType type) { + switch (type) { + case IPMI: { + int position = buffer.position(); + byte netFn = AbstractWireable.getBits(buffer.get(position + 1), 2, IpmiNetworkFunctionCode.MASK); + IpmiNetworkFunctionCode networkFunctionCode = IpmiCode.fromByte(IpmiNetworkFunctionCode.class, (byte) (netFn & ~1)); + byte commandNameByte = buffer.get(position + 5); + IpmiCommandName commandName = IpmiCommandName.fromNetFunctionAndCode(networkFunctionCode, commandNameByte); + boolean isResponse = (netFn & 1) != 0; + return isResponse ? commandName.newIpmiResponse() : commandName.newIpmiRequest(); + } + case RMCPOpenSessionRequest: + return new RmcpPlusOpenSessionRequest(); + case RMCPOpenSessionResponse: + return new RmcpPlusOpenSessionResponse(); + case RAKPMessage1: + return new RakpMessage1(); + case RAKPMessage2: + return new RakpMessage2(); + case RAKPMessage3: + return new RakpMessage3(); + case RAKPMessage4: + return new RakpMessage4(); + default: + throw new UnsupportedOperationException("Unsupported IPMI payload type: " + type); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/Ipmi20Ipv4SessionWrapper.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/Ipmi20Ipv4SessionWrapper.java new file mode 100644 index 0000000..e447303 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/Ipmi20Ipv4SessionWrapper.java @@ -0,0 +1,219 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.IpmiPayload; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.IpmiPayloadType; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiAuthenticationCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiConfidentialityCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiIntegrityCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.confidentiality.IpmiConfidentiality; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity.IpmiIntegrity; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteOrderUtils; + +/** + * See IPMIv2 Section 13.6 + */ +public class Ipmi20Ipv4SessionWrapper extends AbstractIpmiSessionWrapper{ + + private static final IpmiSessionAuthenticationType AUTH_TYPE = IpmiSessionAuthenticationType.RMCPP; + + @Override + public int getWireLength(IpmiPacketContext context) { + IpmiPayload payload = getIpmiPayload(); + int rawDataLength = payload.getWireLength(context); + int encryptDataLength = rawDataLength; + IpmiConfidentialityCode confidentialityCode = getConfidentialityCode(context.getIpmiSession()); + if (!IpmiConfidentialityCode.NONE.equals(confidentialityCode)) { + IpmiConfidentiality confidentiality = confidentialityCode.newIpmiConfidentiality(); + if (confidentiality == null) { + throw new UnsupportedOperationException("Such confidentiality algorithm not support"); + } + encryptDataLength = confidentiality.getEncryptedLength(rawDataLength); + } + int integrityDataLength = 0; + IpmiIntegrityCode integrityCode = getIntegrityCode(context.getIpmiSession()); + if (!IpmiIntegrityCode.NONE.equals(integrityCode)) { + int padLength = 4 - (12 + encryptDataLength + 1 + 1) % 4; + IpmiIntegrity ipmiIntegrity = integrityCode.newIpmiIntegrity(); + if (ipmiIntegrity == null) { + throw new UnsupportedOperationException("Such integrity algorithm not support"); + } + integrityDataLength = padLength + 1 + 1 + ipmiIntegrity.getHashLength(); + } + return 12 + encryptDataLength + integrityDataLength; + } + + public IpmiConfidentialityCode getConfidentialityCode(IpmiSession session) { + if (getIpmiSessionId() == 0) { + return IpmiConfidentialityCode.NONE; + } + return session.getConfidentialityAlgorithm(); + } + + public IpmiAuthenticationCode getAuthenticationCode(IpmiSession session) { + if (getIpmiSessionId() == 0) { + return IpmiAuthenticationCode.RAKP_NOME; + } + return session.getAuthenticationAlgorithm(); + } + + public IpmiIntegrityCode getIntegrityCode(IpmiSession session) { + if (getIpmiSessionId() == 0) { + return IpmiIntegrityCode.NONE; + } + return session.getIntegrityAlgorithm(); + } + + @Override + public void toWire(IpmiPacketContext context, ByteBuffer buffer) { + IpmiSession session = context.getIpmiSession(); + + IpmiConfidentialityCode confidentialityCode = getConfidentialityCode(session); + IpmiIntegrityCode integrityCode = getIntegrityCode(session); + + IpmiPayload payload = getIpmiPayload(); + + ByteBuffer integrityInput = buffer.duplicate(); + + buffer.put(AUTH_TYPE.getCode()); + + boolean encrypted = !IpmiConfidentialityCode.NONE.equals(confidentialityCode); + boolean authenticated = !IpmiIntegrityCode.NONE.equals(integrityCode); + byte tmp = setBits((byte) 0, payload.getPayloadType().getCode(), IpmiPayloadType.MASK); + tmp = setBits(tmp, 6, 0x1, authenticated ? 1 : 0); + tmp = setBits(tmp, 7, 0x1, encrypted ? 1 : 0); + buffer.put(tmp); + + if (IpmiPayloadType.OEM_EXPLICIT.equals(payload.getPayloadType())) { + throw new UnsupportedOperationException("OEM explicit payload not supported"); + } + + ByteOrderUtils.writeLeInt(buffer, getIpmiSessionId()); + ByteOrderUtils.writeLeInt(buffer, getIpmiSessionSequenceNumber()); + + IpmiConfidentiality confidentiality = confidentialityCode.newIpmiConfidentiality(); + int rawDataLength = payload.getWireLength(context); + if (confidentiality == null) { + throw new UnsupportedOperationException("Such confidentiality algorithm not support"); + } + int encryptDataLength = confidentiality.getEncryptedLength(rawDataLength); + ByteOrderUtils.writeLeChar(buffer, (char) encryptDataLength); + + try { + if (encrypted) { + ByteBuffer raw = ByteBuffer.allocate(rawDataLength); + payload.toWire(context, raw); + raw.flip(); + confidentiality.encrypt(session, raw, buffer); + } else { + payload.toWire(context, buffer); + } + + if (authenticated) { + int padLength = 4 - (12 + encryptDataLength + 1 + 1) % 4; + byte[] pad = new byte[padLength]; + for (int i = 0; i < padLength; i++) { + pad[i] = (byte) 0xFF; + } + buffer.put(pad); + buffer.put((byte) padLength); + buffer.put((byte) 0x07); + integrityInput.limit(buffer.position()); + IpmiIntegrity ipmiIntegrity = integrityCode.newIpmiIntegrity(); + if (ipmiIntegrity == null) { + throw new UnsupportedOperationException("Such integrity algorithm not support"); + } + ipmiIntegrity.setKey(session.getK1()); + ipmiIntegrity.setData(ByteOrderUtils.readBytes(integrityInput, integrityInput.limit() - integrityInput.position())); + byte[] integrityData = ipmiIntegrity.getHash(); + buffer.put(integrityData); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void fromWire(IpmiPacketContext context, ByteBuffer buffer) { + IpmiSession session = context.getIpmiSession(); + ByteBuffer integrityInput = buffer.duplicate(); + + IpmiSessionAuthenticationType type = IpmiCode.fromByte(IpmiSessionAuthenticationType.class, buffer.get()); + if (!IpmiSessionAuthenticationType.RMCPP.equals(type)) { + throw new UnsupportedOperationException("Unsupported authentication type: " + type); + } + + byte tmp = buffer.get(); + boolean encrypted = getBits(tmp, 7, 0x01) == 1; + boolean authenticated = getBits(tmp, 6, 0x01) == 1; + IpmiPayloadType payloadType = IpmiCode.fromByte(IpmiPayloadType.class, getBits(tmp, IpmiPayloadType.MASK)); + + setIpmiSessionId(ByteOrderUtils.readLeInt(buffer)); + setIpmiSessionSequenceNumber(ByteOrderUtils.readLeInt(buffer)); + + int payloadLength = ByteOrderUtils.readLeChar(buffer); + + ByteBuffer encryptedBuffer = buffer.duplicate(); + encryptedBuffer.limit(encryptedBuffer.position() + payloadLength); + buffer.position(encryptedBuffer.position() + payloadLength); + + try { + ByteBuffer decryptedBuffer; + if (encrypted) { + IpmiConfidentialityCode confidentialityCode = getConfidentialityCode(session); + IpmiConfidentiality confidentiality = confidentialityCode.newIpmiConfidentiality(); + if (confidentiality == null) { + throw new UnsupportedOperationException("Such confidentiality algorithm not support"); + } + decryptedBuffer = confidentiality.decrypt(session, encryptedBuffer); + } else { + decryptedBuffer = encryptedBuffer; + } + IpmiPayload payload = newIpmiPayload(decryptedBuffer, payloadType); + payload.fromWire(context, decryptedBuffer); + setIpmiPayload(payload); + + + if (authenticated) { + int padLength = 4 - (12 + payloadLength + 1 + 1) % 4; + ignoreBytes(buffer, padLength + 2); + integrityInput.limit(buffer.position()); + IpmiIntegrityCode ipmiIntegrityCode = session.getIntegrityAlgorithm(); + IpmiIntegrity ipmiIntegrity = ipmiIntegrityCode.newIpmiIntegrity(); + if (ipmiIntegrity == null) { + throw new UnsupportedOperationException("Such integrity algorithm not support"); + } + ipmiIntegrity.setKey(session.getK1()); + ipmiIntegrity.setData(ByteOrderUtils.readBytes(integrityInput, integrityInput.limit() - integrityInput.position())); + byte[] expectIntegrityData = ipmiIntegrity.getHash(); + byte[] actualIntegrityData = ByteOrderUtils.readBytes(buffer, ipmiIntegrity.getHashLength()); + if (!Arrays.equals(expectIntegrityData, actualIntegrityData)) { + throw new RuntimeException("Integrity check failed"); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiChannelMediumCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiChannelMediumCode.java new file mode 100644 index 0000000..c24955f --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiChannelMediumCode.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 6.5 + */ +public enum IpmiChannelMediumCode implements IpmiCode.Code { + IPMB_I2C(0x1), + ICMB_v1_0(0x2), + ICMB_v0_9(0x3), + LAN_802_3(0x4), + Serial_Modem(0x5), + LAN_Other(0x6), + PCI_SMBus(0x7), + SMBus_v1(0x8), + SMBus_v2(0x9), + USB_v1(0xA), + USB_v2(0xB), + System_Interface(0xC); + private final byte code; + + public static final int MASK = 0x7F; + + private IpmiChannelMediumCode(int code) { + this.code = ByteConvertUtils.checkCastByte(code); + } + + @Override + public byte getCode() { + return code; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiChannelNumberCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiChannelNumberCode.java new file mode 100644 index 0000000..f9bf3ae --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiChannelNumberCode.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 6.3 + */ +public enum IpmiChannelNumberCode implements IpmiCode.Code { + Primary_IPMB(0x0), + C1(0x1), + C2(0x2), + C3(0x3), + C4(0x4), + C5(0x5), + C6(0x6), + C7(0x7), + C8(0x8), + C9(0x9), + C10(0xA), + C11(0xB), + PRESENT(0xE), + System_Interface(0xF); + + private final byte code; + + public static final int MASK = 0xF; + + private IpmiChannelNumberCode(int code) { + this.code = ByteConvertUtils.checkCastByte(code); + } + + @Override + public byte getCode() { + return code; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiChannelPrivilegeLevel.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiChannelPrivilegeLevel.java new file mode 100644 index 0000000..fffbc49 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiChannelPrivilegeLevel.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 6.8 + */ +public enum IpmiChannelPrivilegeLevel implements IpmiCode.Code { + Unprotected(0x00), + Callback(0x01), + User(0x02), + Operator(0x03), + Administrator(0x04), + OEM(0x05); + private final byte code; + + public static final byte MASK = 0x07; + + private IpmiChannelPrivilegeLevel(int code) { + this.code = ByteConvertUtils.checkCastByte(code); + } + + @Override + public byte getCode() { + return code; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiChannelProtocolCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiChannelProtocolCode.java new file mode 100644 index 0000000..52db715 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiChannelProtocolCode.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 6.4 + */ +public enum IpmiChannelProtocolCode implements IpmiCode.Code { + IPMB_v1_0(0x01), + ICMB_v1_0(0x02), + IPMI_SMBus(0x04), + KCS(0x05), + SMIC(0x06), + BT_10(0x07), + BT_15(0x08), + TMode(0x09), + OEM1(0x1C), + OEM2(0x1D), + OEM3(0x1E), + OEM4(0x1F); + + + private final byte code; + public static final int MASK = 0xFF; + + private IpmiChannelProtocolCode(int code) { + this.code = ByteConvertUtils.checkCastByte(code); + } + + @Override + public byte getCode() { + return code; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiCommandName.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiCommandName.java new file mode 100644 index 0000000..0f244d6 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiCommandName.java @@ -0,0 +1,118 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.IpmiRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.IpmiResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.chassis.GetChassisStatusRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.chassis.GetChassisStatusResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.messaging.CloseSessionRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.messaging.CloseSessionResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.messaging.GetChannelAuthenticationCapabilitiesRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.messaging.GetChannelAuthenticationCapabilitiesResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.GetSdrRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.GetSdrResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.GetSensorReadingRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.GetSensorReadingResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.ReserveSdrRepositoryRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.ReserveSdrRepositoryResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Appendix G + */ +public enum IpmiCommandName implements IpmiCode.Code { + + GetChassisStatus("Get Chassis Status", IpmiNetworkFunctionCode.Chassis, 0x01, + IpmiChannelPrivilegeLevel.User, GetChassisStatusRequest.class, GetChassisStatusResponse.class), + GetChannelAuthenticationCapabilities("Get Channel Authentication Capabilities", IpmiNetworkFunctionCode.App, + 0x38, IpmiChannelPrivilegeLevel.User, GetChannelAuthenticationCapabilitiesRequest.class, GetChannelAuthenticationCapabilitiesResponse.class), + CloseSession("Close Session", IpmiNetworkFunctionCode.App, 0x3C, IpmiChannelPrivilegeLevel.Callback, + CloseSessionRequest.class, CloseSessionResponse.class), + ReserveSdrRepository("Reserve SDR Repository", IpmiNetworkFunctionCode.Storage, 0x22, IpmiChannelPrivilegeLevel.User, + ReserveSdrRepositoryRequest.class, ReserveSdrRepositoryResponse.class), + GetSdr("Get SDR", IpmiNetworkFunctionCode.Storage, 0x23, IpmiChannelPrivilegeLevel.User, GetSdrRequest.class, GetSdrResponse.class), + GetSensorReading("Get Sensor Reading", IpmiNetworkFunctionCode.Sensor_Event, 0x2D, IpmiChannelPrivilegeLevel.User, GetSensorReadingRequest.class, GetSensorReadingResponse.class); + + private final String name; + private final IpmiNetworkFunctionCode networkFunction; + private final byte code; + private final IpmiChannelPrivilegeLevel privilegeLevel; + private final Class<? extends IpmiRequest> requestType; + private final Class<? extends IpmiResponse> responseType; + + private IpmiCommandName(String name, IpmiNetworkFunctionCode networkFunction, int code, IpmiChannelPrivilegeLevel privilegeLevel, + Class<? extends IpmiRequest> requestType, Class<? extends IpmiResponse> responseType) { + this.name = name; + this.networkFunction = networkFunction; + this.code = ByteConvertUtils.checkCastByte(code); + this.privilegeLevel = privilegeLevel; + this.requestType = requestType; + this.responseType = responseType; + } + + @Override + public byte getCode() { + return code; + } + + public IpmiNetworkFunctionCode getNetworkFunction() { + return networkFunction; + } + + public static IpmiCommandName fromNetFunctionAndCode(IpmiNetworkFunctionCode networkFunction, byte code) { + for (IpmiCommandName command : values()) { + if (command.networkFunction == networkFunction && command.code == code) { + return command; + } + } + throw new UnsupportedOperationException("Unsupported command: " + networkFunction + " " + ByteConvertUtils.byteToInt(code)); + } + + public static IpmiResponse getResponseByRequest(Class<? extends IpmiRequest> request) { + for (IpmiCommandName command : values()) { + if (command.requestType.isInstance(request)) { + IpmiResponse response = command.newIpmiResponse(); + if (command.responseType.isInstance(response)) { + return response; + } + } + } + throw new UnsupportedOperationException("Unsupported request: " + request); + } + + public IpmiResponse newIpmiResponse() { + try { + return responseType.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public IpmiRequest newIpmiRequest() { + try { + return requestType.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiCompletionCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiCompletionCode.java new file mode 100644 index 0000000..9054ee5 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiCompletionCode.java @@ -0,0 +1,75 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 5.2 + */ +public enum IpmiCompletionCode implements IpmiCode.Code { + CompletedNormally(0x00, "Command Completed Normally."), + NodeBusy(0xC0, "Node Busy. Command could not be processed because command processing resources are temporarily unavailable."), + InvalidCommand(0xC1, "Invalid Command. Used to indicate an unrecognized or unsupported command."), + InvalidForGivenLUN(0xC2, "Command invalid for given LUN."), + Timeout(0xC3, "Timeout while processing command. Response unavailable."), + OutOfSpace(0xC4, "Out of space. Command could not be completed because of a lack of storage space required to execute the given command operation."), + ReservationCanceled(0xC5, "Reservation Canceled or Invalid Reservation ID."), + RequestDataTruncated(0xC6, "Request data truncated."), + RequestDataLengthInvalid(0xC7, "Request data length invalid."), + RequestDataFieldLengthExceed(0xC8, "Request data field length limit exceeded."), + ParameterOutOfRange(0xC9, "Parameter out of range. "), + CannotReturnRequestedData(0xCA, "Cannot return number of requested data bytes."), + RequestDataNotPresent(0xCB, "Requested Sensor, data, or record not present"), + InvalidDataFieldRequest(0xCC, "Invalid data field in Request"), + CommandIllegalForSDR(0xCD, "Command illegal for specified sensor or record type."), + CommandResponseNotProvided(0xCE, "Command response could not be provided."), + DuplicateRequest(0xCF, "Cannot execute duplicated request. "), + SDRRepositoryInUpdateMode(0xD0, "Command response could not be provided. SDR Repository in update mode"), + FirmwareUpdateMode(0xD1, "Command response could not be provided. Device in firmware update mode."), + BMCInitialization(0xD2, "Command response could not be provided. BMC initialization or initialization agent in progress."), + DestinationUnavailable(0xD3, "Destination unavailable. Cannot deliver request to selected destination."), + InsufficientPrivilege(0xD4, "Cannot execute command due to insufficient privilege level or other security-based restriction (e.g. disabled for 'firmware firewall)"), + NotSupportedCommand(0xD5, "Cannot execute command. Command, or request parameter(s), not supported in present state."), + ParameterIllegal(0xD6, "Cannot execute command. Parameter is illegal because command sub-function has been disabled or is unavailable (e.g. disabled for 'firmware firewall')."), + UnspecifiedError(0xFF, "Unspecified error.") + ; + + + private final byte code; + private final String description; + + public static final int MASK = 0xFF; + + private IpmiCompletionCode(int code, String description) { + this.code = ByteConvertUtils.checkCastByte(code); + this.description = description; + } + + @Override + public byte getCode() { + return code; + } + + @Override + public String getDescription() { + return description; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiLun.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiLun.java new file mode 100644 index 0000000..be8d20c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiLun.java @@ -0,0 +1,44 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 13.8 + */ +public enum IpmiLun implements IpmiCode.Code { + L0(0), L1(1), L2(2), L3(3); + public static final int MASK = 0x03; + private final byte code; + + private IpmiLun(int value) { + this.code = ByteConvertUtils.checkCastByte(value); + } + + @Override + public byte getCode() { + return code; + } + + public int getValue() { + return getCode() & MASK; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiNetworkFunctionCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiNetworkFunctionCode.java new file mode 100644 index 0000000..84b2e22 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiNetworkFunctionCode.java @@ -0,0 +1,59 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.IpmiCommand; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.IpmiResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 5.1 + */ +public enum IpmiNetworkFunctionCode implements IpmiCode.Code { + + Chassis(0x00), + Bridge(0x02), + Sensor_Event(0x04), + App(0x06), + Firmware(0x08), + Storage(0x0A), + Transport(0x0C), + GroupExtension(0x2C), + OEM_Group(0x2E); + + private final byte code; + + public static final int MASK = 0x3F; + + private IpmiNetworkFunctionCode(int code) { + this.code = ByteConvertUtils.checkCastByte(code); + } + + @Override + public byte getCode() { + return code; + } + + public int getCode(IpmiCommand command) { + if (command instanceof IpmiResponse) { + return (code | 0x1); + } + return code; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiSessionAuthenticationType.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiSessionAuthenticationType.java new file mode 100644 index 0000000..a1c19d6 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiSessionAuthenticationType.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 13.6 + */ +public enum IpmiSessionAuthenticationType implements IpmiCode.Code { + NONE(0), + MD2(1), + MD5(2), + PASSWORD(4), + OEM_PROPRIETARY(5), + RMCPP(6); + private final byte code; + + private IpmiSessionAuthenticationType(int code) { + this.code = ByteConvertUtils.checkCastByte(code); + } + + @Override + public byte getCode() { + return code; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiSessionWrapper.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiSessionWrapper.java new file mode 100644 index 0000000..5215c0d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/IpmiSessionWrapper.java @@ -0,0 +1,41 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiEncapsulation; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.IpmiPayload; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.rmcp.RmcpData; + +/** + * Ipmi Session Wrapper Interface + */ +public interface IpmiSessionWrapper extends RmcpData, IpmiEncapsulation.Encapsulation { + + int getIpmiSessionId(); + + void setIpmiSessionId(int ipmiSessionId); + + int getIpmiSessionSequenceNumber(); + + void setIpmiSessionSequenceNumber(int ipmiSessionSequenceNumber); + + IpmiPayload getIpmiPayload(); + + void setIpmiPayload(IpmiPayload ipmiPayload); + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/AbstractIpmiCommand.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/AbstractIpmiCommand.java new file mode 100644 index 0000000..7c59be2 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/AbstractIpmiCommand.java @@ -0,0 +1,127 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiLun; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.AbstractIpmiPayload; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.IpmiPayloadType; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity.IntegrityUtils; + +/** + * Abstract IPMI command. See IPMIv2 Section 13.8 + */ +public abstract class AbstractIpmiCommand extends AbstractIpmiPayload implements IpmiCommand { + private static final int SEQUENCE_NUMBER_MASK = 0x3F; + private byte rsAddress = 0x20; + private IpmiLun rsLun = IpmiLun.L0; + private byte rqAddress = (byte) 0x81; + private IpmiLun rqLun = IpmiLun.L0; + private byte sequenceNumber; + + @Override + public IpmiPayloadType getPayloadType() { + return IpmiPayloadType.IPMI; + } + + @Override + public byte getRsAddress() { + return rsAddress; + } + + @Override + public IpmiLun getRsLun() { + return rsLun; + } + + @Override + public byte getRqAddress() { + return rqAddress; + } + + @Override + public IpmiLun getRqLun() { + return rqLun; + } + + @Override + public void setRqLun(IpmiLun rqLun) { + this.rqLun = rqLun; + } + + @Override + public byte getSequenceNumber() { + return sequenceNumber; + } + + @Override + public void setSequenceNumber(byte sequenceNumber) { + this.sequenceNumber = (byte) (sequenceNumber & SEQUENCE_NUMBER_MASK); + } + + @Override + public int getWireLength(IpmiPacketContext context) { + return 7 + getDataWireLength(context); + } + + public abstract int getDataWireLength(IpmiPacketContext context); + + public abstract void toWireData(IpmiPacketContext context, ByteBuffer buffer); + + @Override + public void toWire(IpmiPacketContext context, ByteBuffer buffer) { + int c1 = buffer.position(); + buffer.put(getRsAddress()); + int netFn = getCommandName().getNetworkFunction().getCode(this); + buffer.put((byte) (netFn << 2 | getRsLun().getValue())); + byte c1sum = IntegrityUtils.calculateChecksum(buffer, c1); + buffer.put(c1sum); + int c2 = buffer.position(); + buffer.put(getRqAddress()); + byte seq = (byte) (getSequenceNumber() & SEQUENCE_NUMBER_MASK); + buffer.put((byte) (seq << 2 | getRqLun().getValue())); + buffer.put(getCommandName().getCode()); + toWireData(context, buffer); + byte c2sum = IntegrityUtils.calculateChecksum(buffer, c2); + buffer.put(c2sum); + } + + public abstract void fromWireData(IpmiPacketContext context, ByteBuffer buffer); + + @Override + public void fromWire(IpmiPacketContext context, ByteBuffer buffer) { + int c1 = buffer.position(); + rqAddress = buffer.get(); + int tmp = buffer.get(); + rqLun = IpmiCode.fromInt(IpmiLun.class, tmp & IpmiLun.MASK); + IntegrityUtils.validChecksum(buffer, c1); + int c2 = buffer.position(); + rsAddress = buffer.get(); + tmp = buffer.get(); + sequenceNumber = (byte) (tmp >>> 2 & SEQUENCE_NUMBER_MASK); + rsLun = IpmiCode.fromInt(IpmiLun.class, tmp & IpmiLun.MASK); + buffer.get(); + buffer.limit(buffer.limit() - 1); + fromWireData(context, buffer); + buffer.limit(buffer.limit() + 1); + IntegrityUtils.validChecksum(buffer, c2); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/AbstractIpmiRequest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/AbstractIpmiRequest.java new file mode 100644 index 0000000..f7b9864 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/AbstractIpmiRequest.java @@ -0,0 +1,31 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; + +/** + * Abstract class for IPMI command request + */ +public abstract class AbstractIpmiRequest extends AbstractIpmiCommand implements IpmiRequest { + @Override + public void fromWireData(IpmiPacketContext context, ByteBuffer buffer) { + throw new UnsupportedOperationException("Not implemented"); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/AbstractIpmiResponse.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/AbstractIpmiResponse.java new file mode 100644 index 0000000..0dff106 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/AbstractIpmiResponse.java @@ -0,0 +1,50 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCompletionCode; + +/** + * Abstract class for IPMI command response + */ +public abstract class AbstractIpmiResponse extends AbstractIpmiCommand implements IpmiResponse { + + public IpmiCompletionCode completionCode; + + @Override + public void toWireData(IpmiPacketContext context, ByteBuffer buffer) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void fromWireData(IpmiPacketContext context, ByteBuffer buffer) { + completionCode = IpmiCode.fromByte(IpmiCompletionCode.class, buffer.get()); + fromResponseData(context, buffer); + } + + @Override + public int getDataWireLength(IpmiPacketContext context) { + return 0; + } + + public abstract void fromResponseData(IpmiPacketContext context, ByteBuffer buffer); + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/IpmiCommand.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/IpmiCommand.java new file mode 100644 index 0000000..ae40681 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/IpmiCommand.java @@ -0,0 +1,45 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiLun; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.IpmiPayload; + +/** + * IPMI Command interface + */ +public interface IpmiCommand extends IpmiPayload { + + byte getRsAddress(); + + IpmiLun getRsLun(); + + byte getRqAddress(); + + IpmiLun getRqLun(); + + void setRqLun(IpmiLun lun); + + byte getSequenceNumber(); + + void setSequenceNumber(byte sequenceNumber); + + IpmiCommandName getCommandName(); + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/IpmiRequest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/IpmiRequest.java new file mode 100644 index 0000000..a9c34e1 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/IpmiRequest.java @@ -0,0 +1,24 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command; + +/** + * Ipmi request interface + */ +public interface IpmiRequest extends IpmiCommand { +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/IpmiResponse.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/IpmiResponse.java new file mode 100644 index 0000000..c3d66f6 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/IpmiResponse.java @@ -0,0 +1,24 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command; + +/** + * Ipmi response interface + */ +public interface IpmiResponse extends IpmiCommand{ +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/chassis/GetChassisStatusRequest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/chassis/GetChassisStatusRequest.java new file mode 100644 index 0000000..3a027e4 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/chassis/GetChassisStatusRequest.java @@ -0,0 +1,49 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command.chassis; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.AbstractIpmiRequest; + +/** + * See IPMIv2 Section 28.2 + */ +public class GetChassisStatusRequest extends AbstractIpmiRequest { + + @Override + public int getDataWireLength(IpmiPacketContext context) { + return 0; + } + + @Override + public void toWireData(IpmiPacketContext context, ByteBuffer buffer) { + + } + + @Override + public void fromWireData(IpmiPacketContext context, ByteBuffer buffer) { + + } + + @Override + public IpmiCommandName getCommandName() { + return IpmiCommandName.GetChassisStatus; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/chassis/GetChassisStatusResponse.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/chassis/GetChassisStatusResponse.java new file mode 100644 index 0000000..aa40759 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/chassis/GetChassisStatusResponse.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.chassis; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.AbstractIpmiResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + + +/** + * See IPMIv2 Section 28.2 + */ +public class GetChassisStatusResponse extends AbstractIpmiResponse { + + public boolean isPowerOn; + public boolean isPowerOverload; + public boolean isPowerInterlock; + public boolean isPowerFault; + public boolean isPowerControlFault; + public String powerRestorePolicy; + public String lastPowerEvent; + public boolean isFanFault; + public boolean isDriveFault; + public boolean isFrontPanelLockoutActive; + + enum PowerRestorePolicy implements IpmiCode.Code { + alwaysOff(0, "Always Off"), + previous(1, "Previous"), + alwaysOn(2, "Always On"), + unknown(3, "Unknown"); + + private final byte code; + private final String description; + + PowerRestorePolicy(int code, String description) { + this.code = ByteConvertUtils.checkCastByte(code); + this.description = description; + } + + @Override + public byte getCode() { + return code; + } + + @Override + public String getDescription() { + return description; + } + } + + enum LastPowerEvent implements IpmiCode.Code { + none(0, "None"), + unknown(1, "Unknown"), + lastPowerOff(2, "Last Power Off"), + lastPowerOn(3, "Last Power On"); + + private final byte code; + private final String description; + + LastPowerEvent(int code, String description) { + this.code = ByteConvertUtils.checkCastByte(code); + this.description = description; + } + + @Override + public byte getCode() { + return code; + } + + @Override + public String getDescription() { + return description; + } + } + + @Override + public IpmiCommandName getCommandName() { + return IpmiCommandName.GetChassisStatus; + } + + @Override + public void fromResponseData(IpmiPacketContext context, ByteBuffer buffer) { + byte t = buffer.get(); + isPowerOn = getBits(t, 0, 0x1) == 1; + isPowerOverload = getBits(t, 1, 0x1) == 1; + isPowerInterlock = getBits(t, 2, 0x1) == 1; + isPowerFault = getBits(t, 3, 0x1) == 1; + isPowerControlFault = getBits(t, 4, 0x1) == 1; + powerRestorePolicy = IpmiCode.fromByte(PowerRestorePolicy.class, getBits(t, 5, 0x3)).getDescription(); + t = buffer.get(); + lastPowerEvent = IpmiCode.fromByte(LastPowerEvent.class, getBits(t, 0, 0x1F)).getDescription(); + t = buffer.get(); + isFrontPanelLockoutActive = getBits(t, 1, 0x1) == 1; + isDriveFault = getBits(t, 2, 0x1) == 1; + isFanFault = getBits(t, 3, 0x1) == 1; + ignoreBytes(buffer, 1); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/messaging/CloseSessionRequest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/messaging/CloseSessionRequest.java new file mode 100644 index 0000000..5a39b8d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/messaging/CloseSessionRequest.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.messaging; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.AbstractIpmiRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteOrderUtils; + + +/** + * See IPMIv2 Section 22.19 + */ +public class CloseSessionRequest extends AbstractIpmiRequest { + + public final int systemSessionId; + + public CloseSessionRequest(int systemSessionId) { + this.systemSessionId = systemSessionId; + } + + @Override + public int getDataWireLength(IpmiPacketContext context) { + return (systemSessionId == 0) ? 5 : 4; + } + + @Override + public void toWireData(IpmiPacketContext context, ByteBuffer buffer) { + ByteOrderUtils.writeLeInt(buffer, systemSessionId); + if (systemSessionId == 0) { + buffer.put((byte) 0); + } + } + + @Override + public IpmiCommandName getCommandName() { + return IpmiCommandName.CloseSession; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/messaging/CloseSessionResponse.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/messaging/CloseSessionResponse.java new file mode 100644 index 0000000..f61807a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/messaging/CloseSessionResponse.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.messaging; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCompletionCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.AbstractIpmiResponse; + +/** + * See IPMIv2 Section 22.19 + */ +public class CloseSessionResponse extends AbstractIpmiResponse { + + @Override + public void fromWireData(IpmiPacketContext context, ByteBuffer buffer) { + byte code = buffer.get(); + if (code == (byte) 0x87) { + throw new RuntimeException("invalid Session ID in request"); + } else if (code == (byte) 0x88) { + throw new RuntimeException("invalid Session Handle in request"); + } else { + completionCode = IpmiCode.fromByte(IpmiCompletionCode.class, code); + } + fromResponseData(context, buffer); + } + + @Override + public void fromResponseData(IpmiPacketContext context, ByteBuffer buffer) { + } + + @Override + public IpmiCommandName getCommandName() { + return IpmiCommandName.CloseSession; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/messaging/GetChannelAuthenticationCapabilitiesRequest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/messaging/GetChannelAuthenticationCapabilitiesRequest.java new file mode 100644 index 0000000..bf1028b --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/messaging/GetChannelAuthenticationCapabilitiesRequest.java @@ -0,0 +1,54 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command.messaging; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiChannelNumberCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiChannelPrivilegeLevel; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.AbstractIpmiRequest; + +/** + * See IPMIv2 Section 22.13 + */ +public class GetChannelAuthenticationCapabilitiesRequest extends AbstractIpmiRequest { + + public IpmiChannelNumberCode channelNumberCode = IpmiChannelNumberCode.PRESENT; + public IpmiChannelPrivilegeLevel channelPrivilegeLevel = IpmiChannelPrivilegeLevel.Administrator; + public int isIpmi20 = 0x1; + + @Override + public int getDataWireLength(IpmiPacketContext context) { + return 2; + } + + @Override + public void toWireData(IpmiPacketContext context, ByteBuffer buffer) { + byte b = 0; + b = setBits(b, 7, 0x1, isIpmi20); + b = setBits(b, 0, IpmiChannelNumberCode.MASK, channelNumberCode.getCode()); + buffer.put(b); + buffer.put(channelPrivilegeLevel.getCode()); + } + + @Override + public IpmiCommandName getCommandName() { + return IpmiCommandName.GetChannelAuthenticationCapabilities; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/messaging/GetChannelAuthenticationCapabilitiesResponse.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/messaging/GetChannelAuthenticationCapabilitiesResponse.java new file mode 100644 index 0000000..8bddeb6 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/messaging/GetChannelAuthenticationCapabilitiesResponse.java @@ -0,0 +1,56 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command.messaging; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiChannelNumberCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.AbstractIpmiResponse; + +/** + * See IPMIv2 Section 22.13 + */ +public class GetChannelAuthenticationCapabilitiesResponse extends AbstractIpmiResponse { + + public IpmiChannelNumberCode channelNumberCode; + public boolean hasExtendedCapabilities; + public byte kgStatus; + public boolean isSupportIpmi20; + public boolean isSupportIpmi15; + + @Override + public void fromResponseData(IpmiPacketContext context, ByteBuffer buffer) { + channelNumberCode = IpmiCode.fromByte(IpmiChannelNumberCode.class, buffer.get()); + byte tmp = buffer.get(); + hasExtendedCapabilities = getBits(tmp, 7, 0x1) == 0x1; + tmp = buffer.get(); + kgStatus = getBits(tmp, 5, 0x1); + tmp = buffer.get(); + isSupportIpmi20 = getBits(tmp, 1, 0x1) == 0x1; + isSupportIpmi15 = getBits(tmp, 0, 0x1) == 0x1; + ignoreBytes(buffer, 4); + } + + @Override + public IpmiCommandName getCommandName() { + return IpmiCommandName.GetChannelAuthenticationCapabilities; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/GetSdrRequest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/GetSdrRequest.java new file mode 100644 index 0000000..2cd2d5f --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/GetSdrRequest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr; + +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.AbstractIpmiRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +import java.nio.ByteBuffer; + +/** + * See IPMIv2 Section 33.12 + */ +public class GetSdrRequest extends AbstractIpmiRequest { + + public static byte HEADER_LENGTH = 5; + + public static int RECORD_ID_START = 0x01; + + final int reservationId; + + final int recordId; + + final byte offset; + + final byte byteToRead; + + public GetSdrRequest(int reservationId, int recordId, byte offset, byte byteToRead) { + this.reservationId = reservationId; + this.recordId = recordId; + this.offset = offset; + this.byteToRead = byteToRead; + } + + @Override + public int getDataWireLength(IpmiPacketContext context) { + return 6; + } + + @Override + public void toWireData(IpmiPacketContext context, ByteBuffer buffer) { + byte[] reservationIdBytes = ByteConvertUtils.intToLsMsByte(reservationId); + buffer.put(reservationIdBytes[0]); + buffer.put(reservationIdBytes[1]); + byte[] recordIdBytes = ByteConvertUtils.intToLsMsByte(recordId); + buffer.put(recordIdBytes[0]); + buffer.put(recordIdBytes[1]); + buffer.put(offset); + buffer.put(byteToRead); + } + + @Override + public IpmiCommandName getCommandName() { + return IpmiCommandName.GetSdr; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/GetSdrResponse.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/GetSdrResponse.java new file mode 100644 index 0000000..cf1ebb7 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/GetSdrResponse.java @@ -0,0 +1,131 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command.sdr; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.AbstractWireable; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiChannelNumberCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiLun; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.AbstractIpmiResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.code.IpmiEntityIdCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.code.IpmiReadingTypeCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.code.IpmiSensorTypeCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.code.IpmiSensorUnitTypeCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteOrderUtils; + +/** + * See IPMIv2 Section 33.12 + */ +public class GetSdrResponse extends AbstractIpmiResponse { + + public int nextRecordId; + public int recordId; + public byte recordType; + public byte recordLength; + public byte sensorOwnerId; + public IpmiLun sensorOwnerLun; + public IpmiChannelNumberCode sensorOwnerChannelNumber; + public byte sensorNumber; + public IpmiEntityIdCode entityIdCode; + public IpmiSensorTypeCode sensorTypeCode; + public IpmiReadingTypeCode readingTypeCode; + public byte analogDataFormat; + public IpmiSensorUnitTypeCode unitTypeCode; + public byte linear; + public int m; + public int b; + public int k1; + public int k2; + public String sensorIdString; + + + @Override + public void fromResponseData(IpmiPacketContext context, ByteBuffer buffer) { + byte[] recordIdBytes = ByteOrderUtils.readBytes(buffer, 2); + nextRecordId = ByteConvertUtils.lsMsByteToInt(recordIdBytes[0], recordIdBytes[1]); + fromSensorRecordHeader(context, buffer); + int remainingLength = buffer.remaining(); + if (remainingLength == 0) { + return; + } + if (recordType != 0x01) { + buffer.position(buffer.limit()); + return; + } + fromRecordKeyBytes(context, buffer); + fromFullSensorRecord(context, buffer); + } + + @Override + public IpmiCommandName getCommandName() { + return IpmiCommandName.GetSdr; + } + + + public void fromSensorRecordHeader(IpmiPacketContext context, ByteBuffer buffer) { + recordId = ByteOrderUtils.readLeChar(buffer); + ignoreBytes(buffer, 1); + recordType = buffer.get(); + recordLength = buffer.get(); + } + + public void fromRecordKeyBytes(IpmiPacketContext context, ByteBuffer buffer) { + sensorOwnerId = buffer.get(); + byte t = buffer.get(); + sensorOwnerLun = IpmiCode.fromByte(IpmiLun.class, AbstractWireable.getBits(t, IpmiLun.MASK)); + sensorOwnerChannelNumber = IpmiCode.fromByte(IpmiChannelNumberCode.class, AbstractWireable.getBits(t, 4, IpmiChannelNumberCode.MASK)); + sensorNumber = buffer.get(); + } + + public void fromFullSensorRecord(IpmiPacketContext context, ByteBuffer buffer) { + entityIdCode = IpmiCode.fromBuffer(IpmiEntityIdCode.class, buffer); + ignoreBytes(buffer, 3); + sensorTypeCode = IpmiCode.fromBuffer(IpmiSensorTypeCode.class, buffer); + readingTypeCode = IpmiCode.fromBuffer(IpmiReadingTypeCode.class, buffer); + ignoreBytes(buffer, 6); + byte t = buffer.get(); + analogDataFormat = AbstractWireable.getBits(t, 6, 0x3); + unitTypeCode = IpmiCode.fromBuffer(IpmiSensorUnitTypeCode.class, buffer); + ignoreBytes(buffer, 1); + linear = buffer.get(); + short mt = buffer.getShort(); + m = ((mt & 0xFF00) >> 8 | (mt & 0xC0) << 2); + m = ByteConvertUtils.getBitsAsSigned(m, 10); + short ba = buffer.getShort(); + b = ((ba & 0xFF00) >> 8 | (ba & 0xC0) << 2); + b = ByteConvertUtils.getBitsAsSigned(b, 10); + ignoreBytes(buffer, 1); + byte rb = buffer.get(); + k1 = AbstractWireable.getBits(rb, 0, 0xF); + k1 = ByteConvertUtils.getBitsAsSigned(k1, 4); + k2 = AbstractWireable.getBits(rb, 4, 0xF); + k2 = ByteConvertUtils.getBitsAsSigned(k2, 4); + ignoreBytes(buffer, 17); + int length = buffer.get() & 0x1F; + if (buffer.remaining() < length) { + length = buffer.remaining(); + } + byte[] sensorIdBytes = ByteOrderUtils.readBytes(buffer, length); + sensorIdString = new String(sensorIdBytes, StandardCharsets.US_ASCII); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/GetSensorReadingRequest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/GetSensorReadingRequest.java new file mode 100644 index 0000000..fddccfe --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/GetSensorReadingRequest.java @@ -0,0 +1,50 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command.sdr; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.AbstractIpmiRequest; + +/** + * See IPMIv2 Section 35.14 + */ +public class GetSensorReadingRequest extends AbstractIpmiRequest { + + final byte sensorNumber; + + public GetSensorReadingRequest(byte sensorNumber) { + this.sensorNumber = sensorNumber; + } + + @Override + public int getDataWireLength(IpmiPacketContext context) { + return 1; + } + + @Override + public void toWireData(IpmiPacketContext context, ByteBuffer buffer) { + buffer.put(sensorNumber); + } + + @Override + public IpmiCommandName getCommandName() { + return IpmiCommandName.GetSensorReading; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/GetSensorReadingResponse.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/GetSensorReadingResponse.java new file mode 100644 index 0000000..a38b661 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/GetSensorReadingResponse.java @@ -0,0 +1,42 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command.sdr; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.AbstractIpmiResponse; + +/** + * See IPMIv2 Section 33.12 + */ +public class GetSensorReadingResponse extends AbstractIpmiResponse { + + public int sensorReading; + + @Override + public void fromResponseData(IpmiPacketContext context, ByteBuffer buffer) { + sensorReading = buffer.get(); + buffer.position(buffer.limit()); + } + + @Override + public IpmiCommandName getCommandName() { + return IpmiCommandName.GetSensorReading; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/ReserveSdrRepositoryRequest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/ReserveSdrRepositoryRequest.java new file mode 100644 index 0000000..e38bf62 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/ReserveSdrRepositoryRequest.java @@ -0,0 +1,44 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command.sdr; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.AbstractIpmiRequest; + +/** + * See IPMIv2 Section 33.11 + */ +public class ReserveSdrRepositoryRequest extends AbstractIpmiRequest { + + @Override + public int getDataWireLength(IpmiPacketContext context) { + return 0; + } + + @Override + public void toWireData(IpmiPacketContext context, ByteBuffer buffer) { + + } + + @Override + public IpmiCommandName getCommandName() { + return IpmiCommandName.ReserveSdrRepository; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/ReserveSdrRepositoryResponse.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/ReserveSdrRepositoryResponse.java new file mode 100644 index 0000000..26a0329 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/ReserveSdrRepositoryResponse.java @@ -0,0 +1,44 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command.sdr; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiCommandName; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.AbstractIpmiResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 33.11 + */ +public class ReserveSdrRepositoryResponse extends AbstractIpmiResponse { + + public int reserveId; + + @Override + public void fromResponseData(IpmiPacketContext context, ByteBuffer buffer) { + byte lsReserveId = buffer.get(); + byte msReserveId = buffer.get(); + this.reserveId = ByteConvertUtils.lsMsByteToInt(lsReserveId, msReserveId); + } + + @Override + public IpmiCommandName getCommandName() { + return IpmiCommandName.ReserveSdrRepository; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/code/IpmiEntityIdCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/code/IpmiEntityIdCode.java new file mode 100644 index 0000000..1867f91 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/code/IpmiEntityIdCode.java @@ -0,0 +1,106 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command.sdr.code; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 43.14 + */ +public enum IpmiEntityIdCode implements IpmiCode.Code { + + Unspecified(0x00, "unspecified"), + Other(0x01, "other"), + Unknown(0x02, "unknown"), + Processor(0x03, "processor"), + Disk_or_DiskBay(0x04, "disk or disk bay"), + Peripheral_Bay(0x05, "peripheral bay"), + System_Management_Module(0x06, "system management module"), + System_Board(0x07, "system board"), + Memory_Module(0x08, "memory module"), + Processor_Module(0x09, "processor module"), + Power_Supply(0x0A, "power supply"), + Add_in_Card(0x0B, "add-in card"), + Front_Panel_Board(0x0C, "front panel board"), + Back_Panel_Board(0x0D, "back panel board"), + Power_System_Board(0x0E, "power system board"), + Drive_Backplane(0x0F, "drive backplane"), + System_Internal_Expansion_Board(0x10, "system internal expansion board"), + Other_System_Board(0x11, "other system board"), + Processor_Board(0x12, "processor board"), + Power_Unit(0x13, "power unit / power domain"), + Power_Module(0x14, "power module / DC-to-DC converter"), + Power_Management(0x15, "power management / power distribution board"), + Chassis_Back_Panel_Board(0x16, "chassis back panel board"), + System_Chassis(0x17, "system chassis"), + Sub_Chassis(0x18, "sub-chassis"), + Other_Chassis_Board(0x19, "other chassis board"), + Disk_Drive(0x1A, "disk drive"), + Peripheral_Bay_2(0x1B, "Peripheral Bay"), + Device_Bay(0x1C, "device bay"), + Fan(0x1D, "fan / cooling device"), + Cooling_Unit(0x1E, "cooling unit / cooling domain"), + Cable_or_Interconnect(0x1F, "cable / interconnect"), + Memory_Device(0x20, "memory device"), + System_Management_Software(0x21, "system management software"), + System_Firmware(0x22, "system firmware (e.g. BIOS / EFI)"), + Operating_System(0x23, "operating system"), + System_Bus(0x24, "system bus"), + Group(0x25, "group"), + Remote_Management_Communication_Device(0x26, "remote management communication device"), + External_Environment(0x27, "external environment"), + Battery(0x28, "battery"), + Processing_Blade(0x29, "processing blade"), + Connectivity_Switch(0x2A, "connectivity switch"), + Processor_Memory_Module(0x2B, "processor memory module"), + IO_Module(0x2C, "I/O module"), + Processor_IO_Module(0x2D, "processor I/O module"), + Management_Controller(0x2E, "management controller"), + Ipmi_Channel(0x2F, "IPMI channel"), + PCI_Bus(0x30, "PCI bus"), + PCI_Express_Bus(0x31, "PCI Express bus"), + SCSI_Bus_Parallel(0x32, "SCSI bus (parallel)"), + SATA_SAS_Bus(0x33, "SATA / SAS bus"), + Processor_Front_Side_Bus(0x34, "processor front-side bus"), + Real_Time_Clock(0x35, "real-time clock"), + Air_Inlet(0x37, "air inlet"), + Air_Inlet_2(0x40, "air inlet"), + Processor_2(0x41, "processor"), + System_Board_2(0x042, "system board"), + ; + + private byte code; + + private String description; + + IpmiEntityIdCode(int code, String description) { + this.code = ByteConvertUtils.checkCastByte(code); + this.description = description; + } + + @Override + public byte getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.description; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/code/IpmiReadingTypeCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/code/IpmiReadingTypeCode.java new file mode 100644 index 0000000..5af43c3 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/code/IpmiReadingTypeCode.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.code; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 42.1 + */ +public enum IpmiReadingTypeCode implements IpmiCode.Code{ + Unspecified(0x00), + Threshold(0x01), + Discrete_1(0x02), + Discrete_2(0x03), + Discrete_3(0x04), + Discrete_4(0x05), + Discrete_5(0x06), + Discrete_6(0x07), + Discrete_7(0x08), + Discrete_8(0x09), + Discrete_9(0x0A), + Discrete_10(0x0B), + Discrete_11(0x0C), + Sensor_Specific(0x6F); + + private byte code; + + IpmiReadingTypeCode(int code) { + this.code = ByteConvertUtils.checkCastByte(code); + } + + @Override + public byte getCode() { + return this.code; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/code/IpmiSensorTypeCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/code/IpmiSensorTypeCode.java new file mode 100644 index 0000000..854eca7 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/code/IpmiSensorTypeCode.java @@ -0,0 +1,93 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.command.sdr.code; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 42.2 + */ +public enum IpmiSensorTypeCode implements IpmiCode.Code { + Reserved(0x00, "Reserved"), + Temperature(0x01, "Temperature"), + Voltage(0x02, "Voltage"), + Current(0x03, "Current"), + Fan(0x04, "Fan"), + Physical_Security(0x05, "Physical Security"), + Platform_Security(0x06, "Platform Security Violation Attempt"), + Processor(0x07, "Processor"), + Power_Supply(0x08, "Power Supply"), + Power_Unit(0x09, "Power Unit"), + Cooling_Device(0x0A, "Cooling Device"), + Other(0x0B, "Other Units-based Sensor"), + Memory(0x0C, "Memory"), + Drive_Slot_Bay(0x0D, "Drive Slot"), + POST_Memory_Resize(0x0E, "POST Memory Resize"), + System_Firmwares(0x0F, "System Firmwares Progress"), + Event_Logging_Disabled(0x10, "Event Logging Disabled"), + Watchdog1(0x11, "Watchdog 1"), + System_Event(0x12, "System Event"), + Critical_Interrupt(0x13, "Critical Interrupt"), + Button(0x14, "Button / Switch"), + Module_Board(0x15, "Module / Board"), + Microcontroller(0x16, "Microcontroller / Coprocessor"), + Add_in_Card(0x17, "Add-in Card"), + Chassis(0x18, "Chassis"), + Chip_Set(0x19, "Chip Set"), + Other_FRU(0x1A, "Other FRU"), + Cable_Interconnect(0x1B, "Cable / Interconnect"), + Terminator(0x1C, "Terminator"), + System_Boot_Initiated(0x1D, "System Boot / Restart Initiated"), + Boot_Error(0x1E, "Boot Error"), + OS_Boot(0x1F, "Base OS Boot / Installation Status"), + OS_Critical_Stop(0x20, "OS Stop / Shutdown"), + Slot_Connector(0x21, "Slot / Connector"), + System_ACPI_Power_State(0x22, "System ACPI Power State"), + Watchdog2(0x23, "Watchdog 2"), + Platform_Alert(0x24, "Platform Alert"), + Entity_Presence(0x25, "Entity Presence"), + Monitor_ASIC(0x26, "Monitor ASIC / IC"), + LAN(0x27, "LAN"), + Management_Subsystem_Health(0x28, "Management Subsystem Health"), + Battery(0x29, "Battery"), + Session_Audit(0x2A, "Session Audit"), + Version_Change(0x2B, "Version Change"), + FRU_State(0x2C, "FRU State"); + + private byte code; + + private String description; + + IpmiSensorTypeCode(int code, String description) { + this.code = ByteConvertUtils.checkCastByte(code); + this.description = description; + } + + @Override + public byte getCode() { + return code; + } + + @Override + public String getDescription() { + return description; + } + + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/code/IpmiSensorUnitTypeCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/code/IpmiSensorUnitTypeCode.java new file mode 100644 index 0000000..a666a55 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/command/sdr/code/IpmiSensorUnitTypeCode.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.sdr.code; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 43.17 + */ +public enum IpmiSensorUnitTypeCode implements IpmiCode.Code { + Unspecified(0x00, "Unspecified"), + Degrees_Celsius(0x01, "Degrees C"), + Degrees_Fahrenheit(0x02, "Degrees F"), + Degrees_Kelvin(0x03, "Degrees K"), + Volts(0x04, "Volts"), + Amps(0x05, "Amps"), + Watts(0x06, "Watts"), + Joules(0x07, "Joules"), + Coulombs(0x08, "Coulombs"), + VA(0x09, "VA"), + Nits(0x0A, "Nits"), + Lumen(0x0B, "Lumen"), + Lux(0x0C, "Lux"), + Candela(0x0D, "Candela"), + kPa(0x0E, "kPa"), + PSI(0x0F, "PSI"), + Newton(0x10, "Newton"), + CFM(0x11, "CFM"), + RPM(0x12, "RPM"), + Hz(0x13, "Hz"), + Microsecond(0x14, "Microsecond"), + Millisecond(0x15, "Millisecond"), + Second(0x16, "Second"), + Minute(0x17, "Minute"), + Hour(0x18, "Hour"), + Day(0x19, "Day"), + Week(0x1A, "Week"), + Mil(0x1B, "Mil"), + Inches(0x1C, "Inches"), + Feet(0x1D, "Feet"), + Cu_Inch(0x1E, "Cu in"), + Cu_Feet(0x1F, "Cu feet"), + Mm(0x20, "mm"), + Cm(0x21, "cm"), + Meter(0x22, "m"), + Cu_Cm(0x23, "Cu cm"), + Cu_Meter(0x24, "Cu m"), + Liters(0x25, "Liters"), + Fluid_Ounce(0x26, "Fluid ounce"), + Radians(0x27, "Radians"), + Steradians(0x28, "Steradians"), + Revolutions(0x29, "Revolutions"), + Cycles(0x2A, "Cycles"), + Gravities(0x2B, "Gravities"), + Ounce(0x2C, "Ounce"), + Pound(0x2D, "Pound"), + Ft_Lb(0x2E, "Ft-lb"), + Oz_Inch(0x2F, "Oz-in"), + Gauss(0x30, "Gauss"), + Gilberts(0x31, "Gilberts"), + Henry(0x32, "Henry"), + Millihenry(0x33, "Millihenry"), + Farad(0x34, "Farad"), + Microfarad(0x35, "Microfarad"), + Ohms(0x36, "Ohms"), + Siemens(0x37, "Siemens"), + Mole(0x38, "Mole"), + Becquerel(0x39, "Becquerel"), + PPM(0x3A, "PPM"), + Reserved(0x3B, "Reserved"), + Decibels(0x3C, "Decibels"), + DbA(0x3D, "DbA"), + DbC(0x3E, "DbC"), + Gray(0x3F, "Gray"), + Sievert(0x40, "Sievert"), + Color_Temp_Deg_K(0x41, "Color temp deg K"), + Bit(0x42, "Bit"), + Kilobit(0x43, "Kilobit"), + Megabit(0x44, "Megabit"), + Gigabit(0x45, "Gigabit"), + Byte(0x46, "Byte"), + Kilobyte(0x47, "Kilobyte"), + Megabyte(0x48, "Megabyte"), + Gigabyte(0x49, "Gigabyte"), + Word(0x4A, "Word"), + Dword(0x4B, "Dword"), + Qword(0x4C, "Qword"), + Line(0x4D, "Line"), + Hit(0x4E, "Hit"), + Miss(0x4F, "Miss"), + Retry(0x50, "Retry"), + Reset(0x51, "Reset"), + Overflow(0x52, "Overflow"), + Underrun(0x53, "Underrun"), + Collision(0x54, "Collision"), + Packets(0x55, "Packets"), + Messages(0x56, "Messages"), + Characters(0x57, "Characters"), + Error(0x58, "Error"), + Correctable_Error(0x59, "Correctable error"), + Uncorrectable_Error(0x5A, "Uncorrectable error"), + Fatal_Error(0x5B, "Fatal error"), + Grams(0x5C, "Grams"); + + private byte code; + + private String description; + + IpmiSensorUnitTypeCode(int code, String description) { + this.code = ByteConvertUtils.checkCastByte(code); + this.description = description; + } + + @Override + public byte getCode() { + return code; + } + + @Override + public String getDescription() { + return description; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/AbstractIpmiPayload.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/AbstractIpmiPayload.java new file mode 100644 index 0000000..601b788 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/AbstractIpmiPayload.java @@ -0,0 +1,26 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.payload; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.AbstractWireable; + +/** + * Abstract ipmi payload + */ +public abstract class AbstractIpmiPayload extends AbstractWireable implements IpmiPayload { +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/AbstractSessionIpmiPayload.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/AbstractSessionIpmiPayload.java new file mode 100644 index 0000000..f4a988d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/AbstractSessionIpmiPayload.java @@ -0,0 +1,63 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.payload; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * Abstract ipmi session payload + */ +public abstract class AbstractSessionIpmiPayload extends AbstractIpmiPayload { + + /** + * See IPMIv2 Section 13.20 + */ + public enum MaximumPrivilegeLevel implements IpmiCode.Code { + UNSPECIFIED(0), + CALLBACK(1), + USER(2), + OPERATOR(3), + ADMINISTRATOR(4), + OEM(5); + + private byte code; + + public static final int MASK = 0xF; + + MaximumPrivilegeLevel(int code){ + this.code = ByteConvertUtils.checkCastByte(code); + } + + @Override + public byte getCode() { + return code; + } + } + + public byte messageTag; + + public byte getMessageTag() { + return messageTag; + } + + public void setMessageTag(byte messageTag) { + this.messageTag = messageTag; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/IpmiPayload.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/IpmiPayload.java new file mode 100644 index 0000000..2f38ea3 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/IpmiPayload.java @@ -0,0 +1,27 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.payload; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.Wireable; + +/** + * Ipmi payload interface + */ +public interface IpmiPayload extends Wireable { + IpmiPayloadType getPayloadType(); +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/IpmiPayloadType.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/IpmiPayloadType.java new file mode 100644 index 0000000..ebdfa11 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/IpmiPayloadType.java @@ -0,0 +1,61 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.payload; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 13.27.3 + */ +public enum IpmiPayloadType implements IpmiCode.Code { + IPMI(0x00, 1, 0), + SOL(0x01, 1, 0), + OEM_EXPLICIT(0x02, -1, -1), + RMCPOpenSessionRequest(0x10, 1, 0), + RMCPOpenSessionResponse(0x11, 1, 0), + RAKPMessage1(0x12, 1, 0), + RAKPMessage2(0x13, 1, 0), + RAKPMessage3(0x14, 1, 0), + RAKPMessage4(0x15, 1, 0), + OEM0(0x20, -1, -1), + OEM1(0x21, -1, -1), + OEM2(0x22, -1, -1), + OEM3(0x23, -1, -1), + OEM4(0x24, -1, -1), + OEM5(0x25, -1, -1), + OEM6(0x26, -1, -1), + OEM7(0x27, -1, -1); + // 6 bits only. + private final byte code; + private final int majorFormat; + private final int minorFormat; + + public static final byte MASK = 0x3f; + + private IpmiPayloadType(int code, int majorFormat, int minorFormat) { + this.code = ByteConvertUtils.checkCastByte(code); + this.majorFormat = majorFormat; + this.minorFormat = minorFormat; + } + + @Override + public byte getCode() { + return code; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/MessageStatusCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/MessageStatusCode.java new file mode 100644 index 0000000..fb2dcde --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/MessageStatusCode.java @@ -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.collector.collect.ipmi2.protocol.ipmi.payload; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 13.24 + */ +public enum MessageStatusCode implements IpmiCode.Code { + NO_ERRORS(0x00, "No errors"), + INSUFFICIENT_RESOURCE(0x01, "Insufficient resources to create a session"), + INVALID_SESSION_ID(0x02, "Invalid session ID"), + INVALID_PAYLOAD_TYPE(0x03, "Invalid payload type"), + INVALID_AUTHENTICATION_ALGORITHM(0x04, "Invalid authentication algorithm"), + INVALID_INTEGRITY_ALGORITHM(0x05, "Invalid integrity algorithm"), + NO_MATCHING_AUTHENTICATION(0x06, "No matching authentication payload"), + NO_MATCHING_INTEGRITY(0x07, "No matching integrity payload"), + INACTIVE_SESSION(0x08, "Inactive session ID"), + INVALID_ROLE(0x09, "Invalid role"), + UNAUTHORIZED_ROLE(0x0A, "No matching authentication payload"), + INSUFFICIENT_RESOURCE_FOR_ROLE(0x0B, "Insufficient resources to create asession at the requested role"), + INVALID_NAME(0x0C, "Invalid name length"), + UNAUTHORIZED_NAME(0x0D, "Unauthorized name"), + UNAUTHORIZED_GUID(0x0E, "Unauthorized GUID. (GUID that BMCsubmitted in RAKP Message 2 was notaccepted by remote console)"), + INVALID_INTEGRITY_CHECK_VALUE(0x0F, "Invalid integrity check value"), + INVALID_CONFIDENTIALITY_ALGORITHM(0x10, "Invalid confidentiality algorithm"), + NO_CIPHER_SUITE_MATCH(0x11, "No Cipher Suite match with proposed security algorithms"), + ILLEGAL_PARAMETER(0x12, "Illegal or unrecognized parameter"); + + public static final int MASK = 0xff; + private final byte code; + private final String description; + + MessageStatusCode(int code, String description) { + this.code = ByteConvertUtils.checkCastByte(code); + this.description = description; + } + + @Override + public byte getCode() { + return code; + } + + @Override + public String getDescription() { + return description; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RakpMessage1.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RakpMessage1.java new file mode 100644 index 0000000..e98f62b --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RakpMessage1.java @@ -0,0 +1,76 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.payload; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteOrderUtils; + +/** + * See IPMIv2 Section 13.20 + */ +public class RakpMessage1 extends AbstractSessionIpmiPayload { + + public int systemSessionId; + + public byte[] consoleRandomNumber; + + public int nameLookup = 0x1; + + public MaximumPrivilegeLevel maximumPrivilegeLevel; + + @Override + public int getWireLength(IpmiPacketContext context) { + String userName = context.getIpmiSession().getUserName(); + return 28 + ((userName == null) ? 0 : userName.length()); + } + + @Override + public void toWire(IpmiPacketContext context, ByteBuffer buffer) { + buffer.put(messageTag); + reservedBytes(buffer, 3); + systemSessionId = context.getIpmiSession().getSystemSessionId(); + ByteOrderUtils.writeLeInt(buffer, systemSessionId); + consoleRandomNumber = context.getIpmiSession().getConsoleRandomNumber(); + buffer.put(consoleRandomNumber); + byte t = setBits((byte) 0, 4, 0x1, nameLookup); + maximumPrivilegeLevel = context.getIpmiSession().getMaximumPrivilegeLevel(); + t = setBits(t, 0, MaximumPrivilegeLevel.MASK, maximumPrivilegeLevel.getCode()); + buffer.put(t); + reservedBytes(buffer, 2); + String userName = context.getIpmiSession().getUserName(); + if (userName != null) { + byte[] usernameBytes = userName.getBytes(StandardCharsets.US_ASCII); + buffer.put((byte) usernameBytes.length); + buffer.put(usernameBytes); + } else { + buffer.put((byte) 0); + } + } + + @Override + public void fromWire(IpmiPacketContext context, ByteBuffer buffer) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public IpmiPayloadType getPayloadType() { + return IpmiPayloadType.RAKPMessage1; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RakpMessage2.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RakpMessage2.java new file mode 100644 index 0000000..064facb --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RakpMessage2.java @@ -0,0 +1,107 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.payload; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.util.Arrays; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiAuthenticationCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.authentication.IpmiAuthentication; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteOrderUtils; + +/** + * See IPMIv2 Section 13.21 + */ +public class RakpMessage2 extends AbstractSessionIpmiPayload { + + public MessageStatusCode messageStatusCode; + + public int consoleSessionId; + public byte[] systemRandom; + public byte[] systemGuid; + public byte[] keyExchangeAuthenticationCode; + + @Override + public int getWireLength(IpmiPacketContext context) { + return 40 + keyExchangeAuthenticationCode.length; + } + + @Override + public void toWire(IpmiPacketContext context, ByteBuffer buffer) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void fromWire(IpmiPacketContext context, ByteBuffer buffer) { + messageTag = buffer.get(); + messageStatusCode = IpmiCode.fromBuffer(MessageStatusCode.class, buffer); + ignoreBytes(buffer, 2); + consoleSessionId = ByteOrderUtils.readLeInt(buffer); + systemRandom = ByteOrderUtils.readBytes(buffer, 16); + systemGuid = ByteOrderUtils.readBytes(buffer, 16); + keyExchangeAuthenticationCode = ByteOrderUtils.readBytes(buffer, buffer.remaining()); + byte[] hash = verifyKeyExchangeAuthenticationCode(context.getIpmiSession()); + if (!Arrays.equals(hash, keyExchangeAuthenticationCode)) { + throw new RuntimeException("Key exchange authentication code mismatch"); + } + } + + @Override + public IpmiPayloadType getPayloadType() { + return IpmiPayloadType.RAKPMessage2; + } + + + public byte[] verifyKeyExchangeAuthenticationCode(IpmiSession session) { + String userName = session.getUserName(); + int length = 58 + ((userName == null) ? 0 : userName.length()); + ByteBuffer buffer = ByteBuffer.allocate(length); + ByteOrderUtils.writeLeInt(buffer, session.getConsoleSessionId()); + ByteOrderUtils.writeLeInt(buffer, session.getSystemSessionId()); + ByteOrderUtils.writeBytes(buffer, session.getConsoleRandomNumber()); + ByteOrderUtils.writeBytes(buffer, systemRandom); + ByteOrderUtils.writeBytes(buffer, systemGuid); + byte t = setBits((byte) 0, 4, 0x1, 1); + MaximumPrivilegeLevel maximumPrivilegeLevel = session.getMaximumPrivilegeLevel(); + t = setBits(t, 0, MaximumPrivilegeLevel.MASK, maximumPrivilegeLevel.getCode()); + buffer.put(t); + if (userName != null) { + byte[] usernameBytes = userName.getBytes(StandardCharsets.US_ASCII); + buffer.put((byte) usernameBytes.length); + buffer.put(usernameBytes); + } else { + buffer.put((byte) 0); + } + IpmiAuthenticationCode authenticationCode = session.getAuthenticationAlgorithm(); + IpmiAuthentication authentication = authenticationCode.newIpmiAuthentication(); + if (authentication == null) { + throw new UnsupportedOperationException("Unsupported authentication code: " + authenticationCode); + } + try { + authentication.setKey(session.getPassword().getBytes(StandardCharsets.US_ASCII)); + authentication.setData(buffer.array()); + return authentication.getHash(); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RakpMessage3.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RakpMessage3.java new file mode 100644 index 0000000..8025ecc --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RakpMessage3.java @@ -0,0 +1,100 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.payload; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiAuthenticationCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.authentication.IpmiAuthentication; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteOrderUtils; + +/** + * See IPMIv2 Section 13.22 + */ +public class RakpMessage3 extends AbstractSessionIpmiPayload{ + + private int systemSessionId; + + private MessageStatusCode messageStatusCode = MessageStatusCode.NO_ERRORS; + private byte[] keyExchangeAuthenticationCode; + + @Override + public int getWireLength(IpmiPacketContext context) { + IpmiSession session = context.getIpmiSession(); + IpmiAuthentication authentication = session.getAuthenticationAlgorithm().newIpmiAuthentication(); + if (authentication == null) { + throw new UnsupportedOperationException("Unsupported authentication code: " + session.getAuthenticationAlgorithm()); + } + return 8 + authentication.getHashLength(); + } + + @Override + public void toWire(IpmiPacketContext context, ByteBuffer buffer) { + buffer.put(messageTag); + buffer.put(messageStatusCode.getCode()); + reservedBytes(buffer, 2); + systemSessionId = context.getIpmiSession().getSystemSessionId(); + ByteOrderUtils.writeLeInt(buffer, systemSessionId); + keyExchangeAuthenticationCode = generateKeyExchangeAuthenticationCode(context.getIpmiSession()); + ByteOrderUtils.writeBytes(buffer, keyExchangeAuthenticationCode); + } + + @Override + public void fromWire(IpmiPacketContext context, ByteBuffer buffer) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public IpmiPayloadType getPayloadType() { + return IpmiPayloadType.RAKPMessage3; + } + + private byte[] generateKeyExchangeAuthenticationCode(IpmiSession session) { + String userName = session.getUserName(); + int length = 22 + ((userName == null) ? 0 : userName.length()); + ByteBuffer buffer = ByteBuffer.allocate(length); + ByteOrderUtils.writeBytes(buffer, session.getSystemRandomNumber()); + ByteOrderUtils.writeLeInt(buffer, session.getConsoleSessionId()); + byte t = setBits((byte) 0, 4, 0x1, 1); + MaximumPrivilegeLevel maximumPrivilegeLevel = session.getMaximumPrivilegeLevel(); + t = setBits(t, 0, MaximumPrivilegeLevel.MASK, maximumPrivilegeLevel.getCode()); + buffer.put(t); + if (userName != null) { + byte[] usernameBytes = userName.getBytes(StandardCharsets.US_ASCII); + buffer.put((byte) usernameBytes.length); + buffer.put(usernameBytes); + } else { + buffer.put((byte) 0); + } + IpmiAuthenticationCode authenticationCode = session.getAuthenticationAlgorithm(); + IpmiAuthentication authentication = authenticationCode.newIpmiAuthentication(); + if (authentication == null) { + throw new UnsupportedOperationException("Unsupported authentication code: " + authenticationCode); + } + try { + authentication.setKey(session.getPassword().getBytes(StandardCharsets.US_ASCII)); + authentication.setData(buffer.array()); + return authentication.getHash(); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RakpMessage4.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RakpMessage4.java new file mode 100644 index 0000000..6b7cf87 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RakpMessage4.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload; + +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.util.Arrays; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiIntegrityCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity.IpmiIntegrity; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteOrderUtils; + +/** + * See IPMIv2 Section 13.23 + */ +public class RakpMessage4 extends AbstractSessionIpmiPayload { + + private MessageStatusCode messageStatusCode; + + public int consoleSessionId; + + public byte[] integrityCheckValue; + + @Override + public int getWireLength(IpmiPacketContext context) { + return 0; + } + + @Override + public void toWire(IpmiPacketContext context, ByteBuffer buffer) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void fromWire(IpmiPacketContext context, ByteBuffer buffer) { + messageTag = buffer.get(); + messageStatusCode = IpmiCode.fromBuffer(MessageStatusCode.class, buffer); + ignoreBytes(buffer, 2); + consoleSessionId = ByteOrderUtils.readLeInt(buffer); + integrityCheckValue = ByteOrderUtils.readBytes(buffer, buffer.remaining()); + byte[] hash = verifyIntegrityCheckValue(context.getIpmiSession()); + if (!Arrays.equals(hash, integrityCheckValue)) { + throw new RuntimeException("integrity check value mismatch"); + } + } + + @Override + public IpmiPayloadType getPayloadType() { + return IpmiPayloadType.RAKPMessage4; + } + + + public byte[] verifyIntegrityCheckValue(IpmiSession session) { + int length = 36; + ByteBuffer buffer = ByteBuffer.allocate(length); + ByteOrderUtils.writeBytes(buffer, session.getConsoleRandomNumber()); + ByteOrderUtils.writeLeInt(buffer, session.getSystemSessionId()); + ByteOrderUtils.writeBytes(buffer, session.getSystemGuid()); + IpmiIntegrityCode ipmiIntegrityCode = session.getIntegrityAlgorithm(); + IpmiIntegrity ipmiIntegrity = ipmiIntegrityCode.newIpmiIntegrity(); + if (ipmiIntegrity == null) { + throw new UnsupportedOperationException("Unsupported authentication code: " + ipmiIntegrityCode); + } + try { + ipmiIntegrity.setKey(session.getSik()); + ipmiIntegrity.setData(buffer.array()); + return ipmiIntegrity.getHash(); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RmcpPlusOpenSessionRequest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RmcpPlusOpenSessionRequest.java new file mode 100644 index 0000000..bbd8cfa --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RmcpPlusOpenSessionRequest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiAuthenticationCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiConfidentialityCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiIntegrityCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.authentication.IpmiAuthenticationAlgorithmWrapper; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.confidentiality.IpmiConfidentialityAlgorithmWrapper; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity.IpmiIntegrityAlgorithmWrapper; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteOrderUtils; + +/** + * See IPMIv2 Section 13.17 + */ +public class RmcpPlusOpenSessionRequest extends AbstractSessionIpmiPayload { + + public AbstractSessionIpmiPayload.MaximumPrivilegeLevel maximumPrivilegeLevel = MaximumPrivilegeLevel.UNSPECIFIED; + + public int consoleSessionId; + + public IpmiAuthenticationCode authenticationCode; + + public IpmiIntegrityCode integrityCode; + + public IpmiConfidentialityCode confidentialityCode; + + @Override + public int getWireLength(IpmiPacketContext context) { + return 32; + } + + @Override + public void toWire(IpmiPacketContext context, ByteBuffer buffer) { + buffer.put(messageTag); + buffer.put(maximumPrivilegeLevel.getCode()); + reservedBytes(buffer, 2); + IpmiSession ipmiSession = context.getIpmiSession(); + consoleSessionId = ipmiSession.getConsoleSessionId(); + authenticationCode = ipmiSession.getAuthenticationAlgorithm(); + integrityCode = ipmiSession.getIntegrityAlgorithm(); + confidentialityCode = ipmiSession.getConfidentialityAlgorithm(); + ByteOrderUtils.writeLeInt(buffer, consoleSessionId); + IpmiAuthenticationAlgorithmWrapper authenticationAlgorithmWrapper = new IpmiAuthenticationAlgorithmWrapper(authenticationCode); + authenticationAlgorithmWrapper.toWire(context, buffer); + IpmiIntegrityAlgorithmWrapper integrityAlgorithmWrapper = new IpmiIntegrityAlgorithmWrapper(integrityCode); + integrityAlgorithmWrapper.toWire(context, buffer); + IpmiConfidentialityAlgorithmWrapper confidentialityAlgorithmWrapper = new IpmiConfidentialityAlgorithmWrapper(confidentialityCode); + confidentialityAlgorithmWrapper.toWire(context, buffer); + } + + @Override + public void fromWire(IpmiPacketContext context, ByteBuffer buffer) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public IpmiPayloadType getPayloadType() { + return IpmiPayloadType.RMCPOpenSessionRequest; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RmcpPlusOpenSessionResponse.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RmcpPlusOpenSessionResponse.java new file mode 100644 index 0000000..08255ac --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/payload/RmcpPlusOpenSessionResponse.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiAuthenticationCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiConfidentialityCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiIntegrityCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteOrderUtils; + +/** + * See IPMIv2 Section 13.18 + */ +public class RmcpPlusOpenSessionResponse extends AbstractSessionIpmiPayload { + + public MessageStatusCode messageStatusCode; + public AbstractSessionIpmiPayload.MaximumPrivilegeLevel maximumPrivilegeLevel; + + public IpmiAuthenticationCode authenticationCode; + + public IpmiConfidentialityCode confidentialityCode; + + public IpmiIntegrityCode integrityCode; + + public int consoleSessionId; + + public int systemSessionId; + + @Override + public int getWireLength(IpmiPacketContext context) { + return 0; + } + + @Override + public void toWire(IpmiPacketContext context, ByteBuffer buffer) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void fromWire(IpmiPacketContext context, ByteBuffer buffer) { + messageTag = buffer.get(); + messageStatusCode = IpmiCode.fromBuffer(MessageStatusCode.class, buffer); + maximumPrivilegeLevel = IpmiCode.fromBufferWithMask(AbstractSessionIpmiPayload.MaximumPrivilegeLevel.class, buffer, MaximumPrivilegeLevel.MASK); + ignoreBytes(buffer, 1); + consoleSessionId = ByteOrderUtils.readLeInt(buffer); + systemSessionId = ByteOrderUtils.readLeInt(buffer); + ignoreBytes(buffer, 4); + authenticationCode = IpmiCode.fromBuffer(IpmiAuthenticationCode.class, buffer); + ignoreBytes(buffer, 3); + ignoreBytes(buffer, 4); + integrityCode = IpmiCode.fromBuffer(IpmiIntegrityCode.class, buffer); + ignoreBytes(buffer, 3); + ignoreBytes(buffer, 4); + confidentialityCode = IpmiCode.fromBuffer(IpmiConfidentialityCode.class, buffer); + ignoreBytes(buffer, 3); + } + + @Override + public IpmiPayloadType getPayloadType() { + return IpmiPayloadType.RMCPOpenSessionResponse; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/AbstractIpmiAlgorithmWrapper.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/AbstractIpmiAlgorithmWrapper.java new file mode 100644 index 0000000..ff3e08d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/AbstractIpmiAlgorithmWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.AbstractWireable; + +/** + * See IPMIv2 Section 13.19 + */ +public abstract class AbstractIpmiAlgorithmWrapper extends AbstractWireable implements IpmiAlgorithm { + @Override + public int getWireLength(IpmiPacketContext context) { + return 8; + } + + @Override + public void toWire(IpmiPacketContext context, ByteBuffer buffer) { + buffer.put(getPayloadType()); + buffer.putShort((short) 0); + buffer.put((byte) 8); + buffer.put(getCode()); + buffer.put((byte) 0); + buffer.putShort((short) 0); + } + + @Override + public void fromWire(IpmiPacketContext context, ByteBuffer buffer) { + buffer.get(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/IpmiAlgorithm.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/IpmiAlgorithm.java new file mode 100644 index 0000000..946fea5 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/IpmiAlgorithm.java @@ -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.collector.collect.ipmi2.protocol.ipmi.security; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; + +/** + * Ipmi Algorithm interface + */ +public interface IpmiAlgorithm extends IpmiCode.Code { + byte getPayloadType(); + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/IpmiAuthenticationCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/IpmiAuthenticationCode.java new file mode 100644 index 0000000..537f7de --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/IpmiAuthenticationCode.java @@ -0,0 +1,62 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.authentication.IpmiAuthentication; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.authentication.None; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.authentication.RakpHmacMd5; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.authentication.RakpHmacSha1; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.authentication.RakpHmacSha256; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 13.28 + */ +public enum IpmiAuthenticationCode implements IpmiCode.Code{ + RAKP_NOME(0x00, None.class), + RAKP_HMAC_SHA1(0x01, RakpHmacSha1.class), + RAKP_HMAC_MD5(0x02, RakpHmacMd5.class), + RAKP_HMAC_SHA256(0x03, RakpHmacSha256.class); + + private final byte code; + + private final Class<? extends IpmiAuthentication> authentication; + + + IpmiAuthenticationCode(int code, Class<? extends IpmiAuthentication> authentication) { + this.code = ByteConvertUtils.checkCastByte(code); + this.authentication = authentication; + } + + @Override + public byte getCode() { + return code; + } + + public IpmiAuthentication newIpmiAuthentication() { + if (authentication == null) { + return null; + } + try { + return authentication.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/IpmiConfidentialityCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/IpmiConfidentialityCode.java new file mode 100644 index 0000000..9d12177 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/IpmiConfidentialityCode.java @@ -0,0 +1,61 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.confidentiality.AesCbc128; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.confidentiality.IpmiConfidentiality; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.confidentiality.None; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.confidentiality.Xrc440; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.confidentiality.Xrc4128; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 13.28.5 + */ +public enum IpmiConfidentialityCode implements IpmiCode.Code { + NONE(0x00, None.class), + AES_CBC_128(0x01, AesCbc128.class), + xRC4_128(0x02, Xrc4128.class), + xRC4_40(0x03, Xrc440.class); + + private final byte code; + + private final Class<? extends IpmiConfidentiality> confidentiality; + + IpmiConfidentialityCode(int code, Class<? extends IpmiConfidentiality> confidentiality) { + this.code = ByteConvertUtils.checkCastByte(code); + this.confidentiality = confidentiality; + } + + @Override + public byte getCode() { + return code; + } + + public IpmiConfidentiality newIpmiConfidentiality() { + if (confidentiality == null) { + return null; + } + try { + return confidentiality.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/IpmiIntegrityCode.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/IpmiIntegrityCode.java new file mode 100644 index 0000000..4cebf8c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/IpmiIntegrityCode.java @@ -0,0 +1,63 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity.HmacMd5128; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity.HmacSha196; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity.HmacSha256128; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity.Md5128; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity.IpmiIntegrity; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity.None; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See IPMIv2 Section 13.28.4 + */ +public enum IpmiIntegrityCode implements IpmiCode.Code{ + NONE(0x00, None.class), + HMAC_SHA1_96(0x01, HmacSha196.class), + HMAC_MD5_128(0x02, HmacMd5128.class), + MD5_128(0x03, Md5128.class), + HMAC_SHA256_128(0x04, HmacSha256128.class); + + private final byte code; + + private final Class<? extends IpmiIntegrity> integrity; + + IpmiIntegrityCode(int code, Class<? extends IpmiIntegrity> integrity) { + this.code = ByteConvertUtils.checkCastByte(code); + this.integrity = integrity; + } + + @Override + public byte getCode() { + return code; + } + + public IpmiIntegrity newIpmiIntegrity() { + if (integrity == null) { + return null; + } + try { + return integrity.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/AbstractIpmiAuthentication.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/AbstractIpmiAuthentication.java new file mode 100644 index 0000000..82c58f2 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/AbstractIpmiAuthentication.java @@ -0,0 +1,59 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.authentication; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * Abstract class for IPMI authentication algorithms. + */ +public abstract class AbstractIpmiAuthentication implements IpmiAuthentication { + + private final Mac mac; + + public AbstractIpmiAuthentication(String algorithm) throws NoSuchAlgorithmException { + this.mac = Mac.getInstance(algorithm); + } + + @Override + public void setKey(byte[] key) throws InvalidKeyException { + SecretKey secretKey = new SecretKeySpec(key, mac.getAlgorithm()); + mac.init(secretKey); + } + + @Override + public void setData(byte[] data) { + mac.update(data); + } + + @Override + public byte[] getHash() { + byte[] hash = mac.doFinal(); + int cutLength = getHashLength(); + if (hash.length > cutLength) { + hash = Arrays.copyOf(hash, cutLength); + } + return hash; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/IpmiAuthentication.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/IpmiAuthentication.java new file mode 100644 index 0000000..e43f0e3 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/IpmiAuthentication.java @@ -0,0 +1,33 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.authentication; + +import java.security.InvalidKeyException; + +/** + * Ipmi authentication interface + */ +public interface IpmiAuthentication { + void setKey(byte[] key) throws InvalidKeyException; + + void setData(byte[] data); + + byte[] getHash(); + + int getHashLength(); +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/IpmiAuthenticationAlgorithmWrapper.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/IpmiAuthenticationAlgorithmWrapper.java new file mode 100644 index 0000000..8179cb5 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/IpmiAuthenticationAlgorithmWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.authentication; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.AbstractIpmiAlgorithmWrapper; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiAuthenticationCode; + +/** + * See IPMIv2 Section 13.19 + */ +public class IpmiAuthenticationAlgorithmWrapper extends AbstractIpmiAlgorithmWrapper { + + private final IpmiAuthenticationCode code; + + private final IpmiAuthentication authentication; + + public IpmiAuthenticationAlgorithmWrapper(IpmiAuthenticationCode code) { + this.code = code; + authentication = code.newIpmiAuthentication(); + } + + @Override + public byte getCode() { + return code.getCode(); + } + + @Override + public byte getPayloadType() { + return 0; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/None.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/None.java new file mode 100644 index 0000000..73b7b68 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/None.java @@ -0,0 +1,45 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.authentication; + +import java.security.InvalidKeyException; + +/** + * See IPMIv2 Section 13.28.2 + */ +public class None implements IpmiAuthentication { + + @Override + public void setKey(byte[] key) throws InvalidKeyException { + } + + @Override + public void setData(byte[] data) { + } + + @Override + public byte[] getHash() { + return new byte[0]; + } + + @Override + public int getHashLength() { + return 0; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/RakpHmacMd5.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/RakpHmacMd5.java new file mode 100644 index 0000000..d5aa608 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/RakpHmacMd5.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.authentication; + +import java.security.NoSuchAlgorithmException; + +/** + * See IPMIv2 Section 13.28.3 + */ +public class RakpHmacMd5 extends AbstractIpmiAuthentication { + public RakpHmacMd5() throws NoSuchAlgorithmException { + super("HmacMD5"); + } + + @Override + public int getHashLength() { + return 16; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/RakpHmacSha1.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/RakpHmacSha1.java new file mode 100644 index 0000000..c579242 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/RakpHmacSha1.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.authentication; + +import java.security.NoSuchAlgorithmException; + +/** + * See IPMIv2 Section 13.28.1 + */ +public class RakpHmacSha1 extends AbstractIpmiAuthentication { + public RakpHmacSha1() throws NoSuchAlgorithmException { + super("HmacSHA1"); + } + + @Override + public int getHashLength() { + return 20; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/RakpHmacSha256.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/RakpHmacSha256.java new file mode 100644 index 0000000..fd591ef --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/authentication/RakpHmacSha256.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.authentication; + +import java.security.NoSuchAlgorithmException; + +/** + * See IPMIv2 Section 13.28.1b + */ +public class RakpHmacSha256 extends AbstractIpmiAuthentication { + public RakpHmacSha256() throws NoSuchAlgorithmException { + super("HmacSHA256"); + } + + @Override + public int getHashLength() { + return 32; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/AbstractIpmiConfidentiality.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/AbstractIpmiConfidentiality.java new file mode 100644 index 0000000..59bad4d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/AbstractIpmiConfidentiality.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.confidentiality; + +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * Abstract class for IPMI confidentiality algorithms. + */ +public abstract class AbstractIpmiConfidentiality implements IpmiConfidentiality { + + public final Cipher cipher; + + public AbstractIpmiConfidentiality(String algorithm) throws NoSuchPaddingException, NoSuchAlgorithmException { + this.cipher = Cipher.getInstance(algorithm); + } + + @Override + public int getBlockSize() { + return cipher.getBlockSize(); + } + + public void init(int mode, SecretKeySpec key, IvParameterSpec iv) throws InvalidAlgorithmParameterException, InvalidKeyException { + cipher.init(mode, key, iv); + } + + public void update(ByteBuffer input, ByteBuffer output) throws ShortBufferException { + cipher.update(input, output); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/AesCbc128.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/AesCbc128.java new file mode 100644 index 0000000..3bb421a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/AesCbc128.java @@ -0,0 +1,106 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.confidentiality; + +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteOrderUtils; + +/** + * See IPMIv2 Section 13.29 + */ +public class AesCbc128 extends AbstractIpmiConfidentiality{ + public AesCbc128() throws NoSuchPaddingException, NoSuchAlgorithmException { + super("AES/CBC/NoPadding"); + } + + @Override + public void init(int mode, byte[] key, byte[] iv) throws InvalidAlgorithmParameterException, InvalidKeyException { + if (key.length > 16) { + key = Arrays.copyOf(key, 16); + } + super.init(mode, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv)); + } + + @Override + public void encrypt(IpmiSession session, ByteBuffer in, ByteBuffer out) throws InvalidAlgorithmParameterException, InvalidKeyException, ShortBufferException { + byte[] iv = IpmiConfidentialityAlgorithmWrapper.generateIv(); + byte[] secretKey = session.getK2(); + out.put(iv); + this.init(Cipher.ENCRYPT_MODE, secretKey, iv); + int padLength = pad(in.remaining() + 1); + this.update(in, out); + if (padLength > 0) { + ByteBuffer pad = ByteBuffer.allocate(padLength + 1); + for (int i = 1; i <= padLength; i++) { + pad.put(ByteConvertUtils.checkCastByte(i)); + } + pad.put(ByteConvertUtils.checkCastByte(padLength)); + pad.flip(); + this.update(pad, out); + } + } + + @Override + public ByteBuffer decrypt(IpmiSession session, ByteBuffer in) throws InvalidAlgorithmParameterException, InvalidKeyException, ShortBufferException { + + ByteBuffer buffer = ByteBuffer.allocate(in.remaining()); + byte[] iv = ByteOrderUtils.readBytes(in, 16); + byte[] secretKey = session.getK2(); + this.init(Cipher.DECRYPT_MODE, secretKey, iv); + this.update(in, buffer); + int padLength = ByteConvertUtils.byteToInt(buffer.get(buffer.position() - 1)); + for (int i = 1; i <= padLength; i++) { + if (ByteConvertUtils.byteToInt(buffer.get(buffer.position() - i - 1)) != padLength - i + 1) { + throw new IllegalArgumentException("Bad pad byte " + i); + } + } + buffer.limit(buffer.position() - padLength - 1); + buffer.flip(); + return buffer; + } + + @Override + public int pad(int length) { + int t = length % 16; + if (t == 0) { + return 0; + } + return 16 - t; + } + + @Override + public int getEncryptedLength(int length) { + return 16 + length + pad(length); + } + + @Override + public void update(ByteBuffer input, ByteBuffer output) throws ShortBufferException { + super.update(input, output); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/IpmiConfidentiality.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/IpmiConfidentiality.java new file mode 100644 index 0000000..72a1aae --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/IpmiConfidentiality.java @@ -0,0 +1,41 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.confidentiality; + +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; + +/** + * Confidentiality algorithm interface + */ +public interface IpmiConfidentiality { + + int getBlockSize(); + + void init(int mode, byte[] key, byte[] iv) throws InvalidAlgorithmParameterException, InvalidKeyException; + + void encrypt(IpmiSession session, ByteBuffer in, ByteBuffer out) throws Exception; + + ByteBuffer decrypt(IpmiSession session, ByteBuffer in) throws Exception; + + int pad(int length); + + int getEncryptedLength(int length); +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/IpmiConfidentialityAlgorithmWrapper.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/IpmiConfidentialityAlgorithmWrapper.java new file mode 100644 index 0000000..2c81911 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/IpmiConfidentialityAlgorithmWrapper.java @@ -0,0 +1,56 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.confidentiality; + +import java.security.SecureRandom; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.AbstractIpmiAlgorithmWrapper; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiConfidentialityCode; + +/** + * See IPMIv2 Section 13.19 + */ +public class IpmiConfidentialityAlgorithmWrapper extends AbstractIpmiAlgorithmWrapper { + private static final byte PAYLOAD_TYPE = 2; + + private final IpmiConfidentialityCode code; + + private final IpmiConfidentiality ipmiConfidentiality; + + public IpmiConfidentialityAlgorithmWrapper(IpmiConfidentialityCode code) { + this.code = code; + ipmiConfidentiality = code.newIpmiConfidentiality(); + } + + @Override + public byte getCode() { + return code.getCode(); + } + + @Override + public byte getPayloadType() { + return PAYLOAD_TYPE; + } + + public static byte[] generateIv() { + byte[] iv = new byte[16]; + new SecureRandom().nextBytes(iv); + return iv; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/None.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/None.java new file mode 100644 index 0000000..f5bfcd3 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/None.java @@ -0,0 +1,58 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.confidentiality; + +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; + +/** + * See IPMIv2 Section 13.28.5 + */ +public class None implements IpmiConfidentiality { + + @Override + public int getBlockSize() { + return 1; + } + + @Override + public void init(int mode, byte[] key, byte[] iv) throws InvalidAlgorithmParameterException, InvalidKeyException { + } + + @Override + public void encrypt(IpmiSession session, ByteBuffer in, ByteBuffer out) { + out.put(in); + } + + @Override + public ByteBuffer decrypt(IpmiSession session, ByteBuffer in) { + return in; + } + + @Override + public int pad(int length) { + return 0; + } + + @Override + public int getEncryptedLength(int length) { + return length; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/Xrc4128.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/Xrc4128.java new file mode 100644 index 0000000..866c876 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/Xrc4128.java @@ -0,0 +1,59 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.confidentiality; + +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; + +/** + * See IPMIv2 Section 13.30. Not implement now. + */ +public class Xrc4128 implements IpmiConfidentiality{ + @Override + public int getBlockSize() { + return 0; + } + + @Override + public void init(int mode, byte[] key, byte[] iv) throws InvalidAlgorithmParameterException, InvalidKeyException { + + } + + @Override + public void encrypt(IpmiSession session, ByteBuffer in, ByteBuffer out) { + return; + } + + @Override + public ByteBuffer decrypt(IpmiSession session, ByteBuffer in) { + return null; + } + + + @Override + public int pad(int length) { + return 0; + } + + @Override + public int getEncryptedLength(int length) { + return 0; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/Xrc440.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/Xrc440.java new file mode 100644 index 0000000..26804bf --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/confidentiality/Xrc440.java @@ -0,0 +1,59 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.confidentiality; + +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; + +/** + * See IPMIv2 Section 13.30. Not implement now. + */ +public class Xrc440 implements IpmiConfidentiality{ + @Override + public int getBlockSize() { + return 0; + } + + @Override + public void init(int mode, byte[] key, byte[] iv) throws InvalidAlgorithmParameterException, InvalidKeyException { + + } + + @Override + public void encrypt(IpmiSession session, ByteBuffer in, ByteBuffer out) { + return; + } + + @Override + public ByteBuffer decrypt(IpmiSession session, ByteBuffer in) { + return null; + } + + + @Override + public int pad(int length) { + return 0; + } + + @Override + public int getEncryptedLength(int length) { + return 0; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/AbstractIpmiIntegrity.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/AbstractIpmiIntegrity.java new file mode 100644 index 0000000..26d1451 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/AbstractIpmiIntegrity.java @@ -0,0 +1,59 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.integrity; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * Abstract class for IPMI integrity algorithms. + */ +public abstract class AbstractIpmiIntegrity implements IpmiIntegrity { + + private final Mac mac; + + public AbstractIpmiIntegrity(String algorithm) throws NoSuchAlgorithmException { + this.mac = Mac.getInstance(algorithm); + } + + @Override + public void setKey(byte[] key) throws InvalidKeyException { + SecretKey secretKey = new SecretKeySpec(key, mac.getAlgorithm()); + mac.init(secretKey); + } + + @Override + public void setData(byte[] data) { + mac.update(data); + } + + @Override + public byte[] getHash() { + byte[] hash = mac.doFinal(); + int cutLength = getHashLength(); + if (hash.length > cutLength) { + hash = Arrays.copyOf(hash, cutLength); + } + return hash; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/HmacMd5128.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/HmacMd5128.java new file mode 100644 index 0000000..935865f --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/HmacMd5128.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity; + +import java.security.NoSuchAlgorithmException; + +/** + * See IPMIv2 Section 13.28.4 + */ +public class HmacMd5128 extends AbstractIpmiIntegrity { + public HmacMd5128() throws NoSuchAlgorithmException { + super("HmacMD5"); + } + + @Override + public int getHashLength() { + return 16; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/HmacSha196.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/HmacSha196.java new file mode 100644 index 0000000..8c4247b --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/HmacSha196.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity; + +import java.security.NoSuchAlgorithmException; + +/** + * See IPMIv2 Section 13.28.4 + */ +public class HmacSha196 extends AbstractIpmiIntegrity{ + public HmacSha196() throws NoSuchAlgorithmException { + super("HmacSHA1"); + } + + @Override + public int getHashLength() { + return 12; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/HmacSha256128.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/HmacSha256128.java new file mode 100644 index 0000000..a166181 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/HmacSha256128.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity; + +import java.security.NoSuchAlgorithmException; + +/** + * See IPMIv2 Section 13.28.4 + */ +public class HmacSha256128 extends AbstractIpmiIntegrity { + public HmacSha256128() throws NoSuchAlgorithmException { + super("HmacSHA256"); + } + + @Override + public int getHashLength() { + return 16; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/IntegrityUtils.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/IntegrityUtils.java new file mode 100644 index 0000000..385c6ab --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/IntegrityUtils.java @@ -0,0 +1,42 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.integrity; + +import java.nio.ByteBuffer; + +/** + * IntegrityUtils for verify checksum + */ +public class IntegrityUtils { + public static byte calculateChecksum(ByteBuffer buffer, int start) { + int checksum = 0; + for (int i = start; i < buffer.position(); i++) { + checksum += buffer.get(i); + } + return (byte) -checksum; + } + + public static void validChecksum(ByteBuffer buffer, int start) { + byte exceptChecksum = calculateChecksum(buffer, start); + byte actualChecksum = buffer.get(); + if (exceptChecksum != actualChecksum) { + throw new IllegalArgumentException("Checksum error, except: " + exceptChecksum + ", actual: " + actualChecksum); + } + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/IpmiIntegrity.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/IpmiIntegrity.java new file mode 100644 index 0000000..fe9fc7c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/IpmiIntegrity.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.integrity; + +import java.security.InvalidKeyException; + +/** + * Ipmi Integrity interface + */ +public interface IpmiIntegrity { + + void setKey(byte[] key) throws InvalidKeyException; + + void setData(byte[] data); + + byte[] getHash(); + + int getHashLength(); + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/IpmiIntegrityAlgorithmWrapper.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/IpmiIntegrityAlgorithmWrapper.java new file mode 100644 index 0000000..aedb4d5 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/IpmiIntegrityAlgorithmWrapper.java @@ -0,0 +1,50 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.integrity; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.AbstractIpmiAlgorithmWrapper; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiIntegrityCode; + +/** + * See IPMIv2 Section 13.19 + */ +public class IpmiIntegrityAlgorithmWrapper extends AbstractIpmiAlgorithmWrapper { + + private static final byte PAYLOAD_TYPE = 1; + + private final IpmiIntegrityCode code; + + private final IpmiIntegrity integrity; + + public IpmiIntegrityAlgorithmWrapper(IpmiIntegrityCode code) { + + this.code = code; + this.integrity = code.newIpmiIntegrity(); + } + + @Override + public byte getCode() { + return code.getCode(); + } + + @Override + public byte getPayloadType() { + return PAYLOAD_TYPE; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/Md5128.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/Md5128.java new file mode 100644 index 0000000..c00d97c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/Md5128.java @@ -0,0 +1,61 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.integrity; + +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +/** + * See IPMIv2 Section 13.28.4. Not implement now. + */ +public class Md5128 implements IpmiIntegrity { + + private final MessageDigest digest; + + + public Md5128() throws NoSuchAlgorithmException { + digest = MessageDigest.getInstance("MD5"); + } + + @Override + public void setKey(byte[] key) throws InvalidKeyException { + return; + } + + @Override + public void setData(byte[] data) { + digest.update(data); + } + + @Override + public byte[] getHash() { + byte[] hash = digest.digest(); + int cutLength = getHashLength(); + if (hash.length > cutLength) { + hash = Arrays.copyOf(hash, cutLength); + } + return hash; + } + + @Override + public int getHashLength() { + return 16; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/None.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/None.java new file mode 100644 index 0000000..cb4e4d9 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/ipmi/security/integrity/None.java @@ -0,0 +1,44 @@ +/* + * 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.collector.collect.ipmi2.protocol.ipmi.security.integrity; + +import java.security.InvalidKeyException; + +/** + * See IPMIv2 Section 13.28.4 + */ +public class None implements IpmiIntegrity { + + @Override + public void setKey(byte[] key) throws InvalidKeyException { + } + + @Override + public void setData(byte[] data) { + } + + @Override + public byte[] getHash() { + return new byte[0]; + } + + @Override + public int getHashLength() { + return 0; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/Packet.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/Packet.java new file mode 100644 index 0000000..25da1e3 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/Packet.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.protocol.rmcp; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiEncapsulation; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.Wireable; + +/** + * RMCP Packet interface + */ +public interface Packet extends Wireable, IpmiEncapsulation.Encapsulation { + + RmcpData getData(); + + <T extends RmcpData> T getData(Class<T> type); + + Packet withData(RmcpData data); + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpData.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpData.java new file mode 100644 index 0000000..aa2a734 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpData.java @@ -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.collector.collect.ipmi2.protocol.rmcp; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.Wireable; + +/** + * RMCP Data interface + */ +public interface RmcpData extends Wireable { + RmcpMessageClass getMessageClass(); + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpMessageClass.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpMessageClass.java new file mode 100644 index 0000000..292e5f9 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpMessageClass.java @@ -0,0 +1,42 @@ +/* + * 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.collector.collect.ipmi2.protocol.rmcp; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See ASF 3.2.2.2 RMCP Header + */ +public enum RmcpMessageClass implements IpmiCode.Code { + + ASF(0x06), IPMI(0x07), OEM(0x08); + private final byte code; + + public static final byte MASK = 0x0F; + + private RmcpMessageClass(int code) { + this.code = ByteConvertUtils.checkCastByte(code); + } + + + @Override + public byte getCode() { + return code; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpMessageStatus.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpMessageStatus.java new file mode 100644 index 0000000..4e53b68 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpMessageStatus.java @@ -0,0 +1,39 @@ +/* + * 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.collector.collect.ipmi2.protocol.rmcp; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See ASF 3.2.2.1 RMCP Acknowledge + */ +public enum RmcpMessageStatus implements IpmiCode.Code { + + REQ(0), ACK(1); + private final byte code; + + private RmcpMessageStatus(int code) { + this.code = ByteConvertUtils.checkCastByte(code); + } + + @Override + public byte getCode() { + return code; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpPacket.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpPacket.java new file mode 100644 index 0000000..cffb2b6 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpPacket.java @@ -0,0 +1,126 @@ +/* + * 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.collector.collect.ipmi2.protocol.rmcp; + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiPacketContext; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.AbstractWireable; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiEncapsulation; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.Ipmi20Ipv4SessionWrapper; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.IpmiSessionAuthenticationType; + +/** + * RMCP Packet See ASF 3.2 Remote Management and Control Protocol (RMCP) + */ +public class RmcpPacket extends AbstractWireable implements Packet{ + + protected static final int RMCP_HEADER_LENGTH = 4; + private final RmcpVersion version = RmcpVersion.ASF_RMCP_1_0; + private byte sequenceNumber = (byte) 0xFF; + private RmcpMessageClass messageClass; + private RmcpMessageStatus messageRole = RmcpMessageStatus.REQ; + private RmcpData data; + + + public RmcpVersion getVersion() { + return version; + } + + public byte getSequenceNumber() { + return sequenceNumber; + } + + public RmcpMessageClass getMessageClass() { + return messageClass; + } + + public RmcpPacket withMessageClass(RmcpMessageClass messageClass) { + this.messageClass = messageClass; + return this; + } + + public RmcpMessageStatus getMessageRole() { + return messageRole; + } + + @Override + public RmcpData getData() { + return data; + } + + @Override + public <T extends RmcpData> T getData(Class<T> type) { + return type.cast(getData()); + } + + @Override + public Packet withData(RmcpData data) { + withMessageClass(data.getMessageClass()); + this.data = data; + return this; + } + + @Override + public <T> T getEncapsulated(Class<T> type) { + if (type.isInstance(this)) { + return type.cast(this); + } + return IpmiEncapsulation.getEncapsulated(type, getData()); + } + + + private void toWireHeader(ByteBuffer buffer) { + buffer.put(getVersion().getCode()); + buffer.put((byte) 0x00); + buffer.put(getSequenceNumber()); + byte messageClassByte = getMessageClass().getCode(); + messageClassByte = AbstractWireable.setBits(messageClassByte, 7, 0x01, getMessageRole().getCode()); + buffer.put(messageClassByte); + } + + @Override + public int getWireLength(IpmiPacketContext context) { + return RMCP_HEADER_LENGTH + getData().getWireLength(context); + } + + @Override + public void toWire(IpmiPacketContext context, ByteBuffer buffer) { + toWireHeader(buffer); + getData().toWire(context, buffer); + } + + @Override + public void fromWire(IpmiPacketContext context, ByteBuffer buffer) { + AbstractWireable.ignoreBytes(buffer, 3); + messageClass = IpmiCode.fromBufferWithMask(RmcpMessageClass.class, buffer, RmcpMessageClass.MASK); + int position = buffer.position(); + if (messageClass != RmcpMessageClass.IPMI) { + throw new UnsupportedOperationException("Unsupported RMCP message class " + messageClass); + } + IpmiSessionAuthenticationType type = IpmiCode.fromByte(IpmiSessionAuthenticationType.class, buffer.get(position)); + if (type == IpmiSessionAuthenticationType.RMCPP) { + data = new Ipmi20Ipv4SessionWrapper(); + } else { + throw new UnsupportedOperationException("Unsupported IPMI 1.5 version"); + } + buffer.position(position); + withData(data); + data.fromWire(context, buffer); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpVersion.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpVersion.java new file mode 100644 index 0000000..b10bcd6 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/protocol/rmcp/RmcpVersion.java @@ -0,0 +1,45 @@ +/* + * 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.collector.collect.ipmi2.protocol.rmcp; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.IpmiCode; +import org.apache.hertzbeat.collector.collect.ipmi2.utils.ByteConvertUtils; + +/** + * See ASF 3.2.2.2 RMCP Header + */ +public enum RmcpVersion implements IpmiCode.Code { + + LEGACY0(0), + LEGACY1(1), + LEGACY2(2), + LEGACY3(3), + LEGACY4(4), + LEGACY5(5), + ASF_RMCP_1_0(6); + final byte code; + + private RmcpVersion(int code) { + this.code = ByteConvertUtils.checkCastByte(code); + } + + @Override + public byte getCode() { + return code; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/utils/ByteCheckUtils.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/utils/ByteCheckUtils.java new file mode 100644 index 0000000..7c7a9b5 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/utils/ByteCheckUtils.java @@ -0,0 +1,41 @@ +/* + * 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.collector.collect.ipmi2.utils; + +import java.nio.ByteBuffer; + +/** + * Byte check utils + */ +public class ByteCheckUtils { + + public static void assertBytesInt(ByteBuffer buffer, int exceptInt) { + int actualInt = buffer.getInt() & 0xFF; + if (actualInt != exceptInt) { + throw new IllegalArgumentException("Expected " + exceptInt + ", but got " + actualInt); + } + } + + public static void assertBytesByte(ByteBuffer buffer, byte exceptByte) { + byte actualByte = buffer.get(); + if (actualByte != exceptByte) { + throw new IllegalArgumentException("Expected " + exceptByte + ", but got " + actualByte); + } + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/utils/ByteConvertUtils.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/utils/ByteConvertUtils.java new file mode 100644 index 0000000..097402a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/utils/ByteConvertUtils.java @@ -0,0 +1,59 @@ +/* + * 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.collector.collect.ipmi2.utils; + +import com.google.common.primitives.UnsignedBytes; + +/** + * Byte convert utils + */ +public class ByteConvertUtils { + + public static int byteToInt(byte b) { + return UnsignedBytes.toInt(b); + } + + public static byte checkCastByte(int i) { + return UnsignedBytes.checkedCast(i); + } + + public static int lsMsByteToInt(byte lsByte, byte msByte) { + return (byteToInt(msByte) << 8) + byteToInt(lsByte); + } + + public static byte[] intToLsMsByte(int i) { + if (i < 0 || i > 0xFFFF) { + throw new IllegalArgumentException("Invalid int value: " + i); + } + byte[] bytes = new byte[2]; + bytes[0] = checkCastByte(i & 0xFF); + bytes[1] = checkCastByte((i >> 8) & 0xFF); + return bytes; + } + + public static int getBitsAsSigned(int value, int n) { + int mask = (1 << n) - 1; + int lowerBits = value & mask; + int signBit = 1 << (n - 1); + if ((lowerBits & signBit) != 0) { + lowerBits |= ~mask; + } + return lowerBits; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/utils/ByteOrderUtils.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/utils/ByteOrderUtils.java new file mode 100644 index 0000000..f00ba90 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ipmi2/utils/ByteOrderUtils.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi2.utils; + +import java.nio.ByteBuffer; + +/** + * Byte order read and write utils + */ +public class ByteOrderUtils { + public static long readLeLong(ByteBuffer buffer) { + long value = 0; + for (int i = 0; i < Long.BYTES; i++) { + value |= ((long) buffer.get()) << (i * 8); + } + return value; + } + + public static void writeLeLong(ByteBuffer buffer, long value) { + for (int i = 0; i < Long.BYTES; i++) { + buffer.put((byte) (value >> (i * 8))); + } + } + + public static int readLeInt(ByteBuffer buffer) { + int value = 0; + for (int i = 0; i < Integer.BYTES; i++) { + value |= (buffer.get() & 0xFF) << (i * 8); + } + return value; + } + + public static void writeLeInt(ByteBuffer buffer, int value) { + for (int i = 0; i < Integer.BYTES; i++) { + buffer.put((byte) (value >> (i * 8))); + } + } + + public static char readLeChar(ByteBuffer buffer) { + char value = 0; + for (int i = 0; i < Character.BYTES; i++) { + value |= ((char) buffer.get()) << (i * 8); + } + return value; + } + + public static void writeLeChar(ByteBuffer buffer, char value) { + for (int i = 0; i < Character.BYTES; i++) { + buffer.put((byte) (value >> (i * 8))); + } + } + + public static byte[] readBytes(ByteBuffer buffer, int size) { + byte[] bytes = new byte[size]; + buffer.get(bytes); + return bytes; + } + + public static void writeBytes(ByteBuffer buffer, byte[] bytes) { + buffer.put(bytes); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/CustomizedJmxFactory.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/CustomizedJmxFactory.java new file mode 100644 index 0000000..ff690ea --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/CustomizedJmxFactory.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.jmx; + + +import java.util.Collections; +import java.util.HashMap; + +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.jmx.kafkajmx.KafkaJmxValidator; + +/** + * Please register the components you need to customize here + */ +public class CustomizedJmxFactory { + + // Map associating app names with their corresponding JmxValidator + private static final Map<String, JmxValidator> VALIDATOR_MAP; + + static { + Map<String, JmxValidator> map = new HashMap<>(); + // Register validator for "kafka" + map.put("kafka", new KafkaJmxValidator()); + + // Future validators for other apps can be added here + // Example for "zookeeper": + // map.put("zookeeper", new ZookeeperJmxValidator()); + + VALIDATOR_MAP = Collections.unmodifiableMap(map); + } + + /** + * Validates the object name using the validator associated with the specified app. + * + * @param app The application name. + * @param objectName The JMX object name to validate. + * @return true if the validator deems the object name valid, false otherwise. + */ + public static boolean validate(String app, String objectName) { + if (StringUtils.isBlank(app) || StringUtils.isBlank(objectName)) { + return false; + } + + JmxValidator validator = VALIDATOR_MAP.get(app.toLowerCase()); + if (validator == null) { + return false; + } + + return validator.isValid(objectName); + } + + /** + * Retrieves the appropriate processor for the given object name using the validator associated with the specified app. + * + * @param app The application name. + * @param objectName The JMX object name for which to retrieve the processor. + * @return The corresponding MbeanProcessor instance, or null if none are applicable. + */ + public static MbeanProcessor getProcessor(String app, String objectName) { + if (StringUtils.isBlank(app) || StringUtils.isBlank(objectName)) { + return null; + } + + JmxValidator validator = VALIDATOR_MAP.get(app.toLowerCase()); + if (validator == null) { + return null; + } + + return validator.getProcessor(objectName); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/JmxClassLoader.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/JmxClassLoader.java new file mode 100644 index 0000000..e0fc8a5 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/JmxClassLoader.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.jmx; + +import lombok.extern.slf4j.Slf4j; + +/** + * custom class loader config for JMX + */ +@Slf4j +public class JmxClassLoader extends ClassLoader { + + private static final String[] WHITE_PRE_LIST = new String[]{ + "java.", + "javax.management.", + "org.apache.hertzbeat.", + "org.springframework.util.", + "com.sun.", + "sun.", + "org.slf4j.", + "jdk.", + "org.w3c.dom." + }; + + public JmxClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + for (String whitePre : WHITE_PRE_LIST) { + if (name.startsWith(whitePre)) { + return super.loadClass(name, resolve); + } + } + log.error("Security vulnerability detection in JMX collect: Forbidden class: {}", name); + throw new ClassNotFoundException("Forbidden unsafe collection request content"); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/JmxCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/JmxCollectImpl.java new file mode 100644 index 0000000..1266cac --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/JmxCollectImpl.java @@ -0,0 +1,343 @@ +/* + * 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.collector.collect.jmx; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanFeatureInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanServerConnection; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.CompositeType; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.naming.Context; +import javax.rmi.ssl.SslRMIClientSocketFactory; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection; +import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier; +import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache; +import org.apache.hertzbeat.collector.collect.common.cache.JmxConnect; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JmxProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.LogUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * jmx protocol acquisition implementation + */ +public class JmxCollectImpl extends AbstractCollect { + + private static final String JMX_URL_PREFIX = "service:jmx:rmi:///jndi/rmi://"; + + private static final String JMX_URL_SUFFIX = "/jmxrmi"; + + private static final String IGNORED_STUB = "/stub/"; + + private static final String SUB_ATTRIBUTE = "->"; + + private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance(); + + private final ClassLoader jmxClassLoader; + + private static final Logger logger = LoggerFactory.getLogger(JmxCollectImpl.class); + + public JmxCollectImpl() { + jmxClassLoader = new JmxClassLoader(ClassLoader.getSystemClassLoader()); + } + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + Assert.isTrue(metrics != null && metrics.getJmx() != null, "JMX collect must have JMX params"); + JmxProtocol jmxProtocol = metrics.getJmx(); + + // Validate JMX URL if provided + String url = jmxProtocol.getUrl(); + if (StringUtils.hasText(url)) { + Assert.doesNotContain(url, IGNORED_STUB, "JMX url prohibit contains stub, please check"); + + // Prevent JNDI injection by validating URL format + validateJmxUrl(url); + } else { + // Validate host and port inputs + String host = jmxProtocol.getHost(); + int port = Integer.parseInt(jmxProtocol.getPort()); + + // Validate host format (only allow valid hostnames or IP addresses) + Assert.isTrue(isValidHostname(host), "Invalid hostname format"); + Assert.isTrue(port > 0 && port <= 65535, "Port must be between 1 and 65535"); + } + } + + /** + * Validate JMX URL + * + * @param url JMX URL to validate + * @throws IllegalArgumentException if URL is potentially malicious + */ + private void validateJmxUrl(String url) throws IllegalArgumentException { + JMXServiceURL serviceUrl; + try { + serviceUrl = new JMXServiceURL(url); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid JMX URL", e); + } + Assert.isTrue("rmi".equalsIgnoreCase(serviceUrl.getProtocol()), "Only service:jmx:rmi protocol is supported"); + + String lowerUrl = url.toLowerCase(Locale.ROOT); + String[] disallowedPatterns = {"ldap:", "iiop:", "nis:", "dns:", "corbaname:", "http:", "https:"}; + for (String pattern : disallowedPatterns) { + if (lowerUrl.contains(pattern)) { + throw new IllegalArgumentException("Potentially unsafe JNDI protocol detected in URL: " + pattern); + } + } + + String lowerPath = serviceUrl.getURLPath().toLowerCase(Locale.ROOT); + if (lowerPath.startsWith("/jndi/") && !lowerPath.startsWith("/jndi/rmi://")) { + throw new IllegalArgumentException("Only rmi JNDI protocol is supported"); + } + + // Check for suspicious patterns + if (url.contains("${") || url.contains("$[") || url.contains(":#")) { + throw new IllegalArgumentException("Potentially malicious pattern detected in JMX URL"); + } + } + + /** + * Validate hostname format + * + * @param hostname Hostname to validate + * @return true if hostname is valid + */ + private boolean isValidHostname(String hostname) { + if (hostname == null || hostname.isEmpty()) { + return false; + } + + // Simplified hostname/IP validation regex + // This regex accepts valid hostnames, IPv4 and IPv6 addresses + String hostnameRegex = "^([a-zA-Z0-9][-a-zA-Z0-9]*\\.)+[a-zA-Z0-9][-a-zA-Z0-9]*$|^(\\d{1,3}\\.){3}\\d{1,3}$|^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$"; + return hostname.matches(hostnameRegex); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(jmxClassLoader); + try { + JmxProtocol jmxProtocol = metrics.getJmx(); + // Whether to use customized JMX + MbeanProcessor processor = null; + if (CustomizedJmxFactory.validate(builder.getApp(), jmxProtocol.getObjectName())) { + processor = CustomizedJmxFactory.getProcessor(builder.getApp(), jmxProtocol.getObjectName()); + if (processor != null) { + processor.preProcess(builder, metrics); + if (processor.isCollectionComplete()) { + return; + } + } + } + // Create a jndi remote connection + JMXConnector jmxConnector = getConnectSession(jmxProtocol); + MBeanServerConnection serverConnection = jmxConnector.getMBeanServerConnection(); + ObjectName objectName = new ObjectName(jmxProtocol.getObjectName()); + + Set<String> attributeNameSet = metrics.getAliasFields().stream() + .map(field -> field.split(SUB_ATTRIBUTE)[0]).collect(Collectors.toSet()); + Set<ObjectInstance> objectInstanceSet = serverConnection.queryMBeans(objectName, null); + for (ObjectInstance objectInstance : objectInstanceSet) { + ObjectName currentObjectName = objectInstance.getObjectName(); + MBeanInfo beanInfo = serverConnection.getMBeanInfo(currentObjectName); + MBeanAttributeInfo[] attrInfos = beanInfo.getAttributes(); + String[] attributes = new String[attributeNameSet.size()]; + attributes = Arrays.stream(attrInfos) + .filter(item -> item.isReadable() && attributeNameSet.contains(item.getName())) + .map(MBeanFeatureInfo::getName) + .toList().toArray(attributes); + AttributeList attributeList = serverConnection.getAttributes(currentObjectName, attributes); + + Map<String, String> attributeValueMap = extractAttributeValue(attributeList); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + if (processor != null) { + processor.process(serverConnection, objectInstance, objectInstanceSet, + currentObjectName, attributeValueMap, valueRowBuilder); + } + for (String aliasField : metrics.getAliasFields()) { + String fieldValue = attributeValueMap.get(aliasField); + valueRowBuilder.addColumn(fieldValue != null ? fieldValue : CommonConstants.NULL_VALUE); + } + builder.addValueRow(valueRowBuilder.build()); + if (processor != null && processor.isCollectionComplete()) { + return; + } + } + } catch (IOException exception) { + String errorMsg = CommonUtil.getMessageFromThrowable(exception); + LogUtil.error(logger, "JMX IOException: {0}", errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + LogUtil.error(logger, "JMX Error: {0}", errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + Thread.currentThread().setContextClassLoader(currentClassLoader); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_JMX; + } + + private Map<String, String> extractAttributeValue(AttributeList attributeList) { + if (attributeList == null || attributeList.isEmpty()) { + throw new RuntimeException("attributeList is empty"); + } + Map<String, String> attributeValueMap = new HashMap<>(attributeList.size()); + for (Attribute attribute : attributeList.asList()) { + Object value = attribute.getValue(); + if (value == null) { + LogUtil.info(logger, "attribute {0} value is null.", attribute.getName()); + continue; + } + if (value instanceof Number || value instanceof String || value instanceof ObjectName + || value instanceof Boolean || value instanceof Date || value instanceof TimeUnit) { + attributeValueMap.put(attribute.getName(), value.toString()); + } else if (value instanceof CompositeData) { + CompositeData compositeData = (CompositeData) value; + CompositeType compositeType = compositeData.getCompositeType(); + for (String typeKey : compositeType.keySet()) { + Object fieldValue = compositeData.get(typeKey); + attributeValueMap.put(attribute.getName() + SUB_ATTRIBUTE + typeKey, fieldValue.toString()); + } + } else if (value instanceof String[]) { + String[] values = (String[]) value; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < values.length; index++) { + builder.append(values[index]); + if (index < values.length - 1) { + builder.append(","); + } + } + attributeValueMap.put(attribute.getName(), builder.toString()); + } else { + LogUtil.warn(logger, "attribute value type {0} not support.", value.getClass().getName()); + } + } + return attributeValueMap; + } + + private JMXConnector getConnectSession(JmxProtocol jmxProtocol) throws IOException { + CacheIdentifier identifier = CacheIdentifier.builder().ip(jmxProtocol.getHost()) + .port(jmxProtocol.getPort()).username(jmxProtocol.getUsername()) + .password(jmxProtocol.getPassword()).build(); + Optional<AbstractConnection<?>> cacheOption = connectionCommonCache.getCache(identifier, true); + JMXConnector conn = null; + if (cacheOption.isPresent()) { + JmxConnect jmxConnect = (JmxConnect) cacheOption.get(); + conn = jmxConnect.getConnection(); + try { + conn.getMBeanServerConnection(); + } catch (Exception e) { + conn = null; + connectionCommonCache.removeCache(identifier); + } + } + if (conn != null) { + return conn; + } + String url; + if (jmxProtocol.getUrl() != null) { + url = jmxProtocol.getUrl(); + // Double check URL format for security + if (!url.startsWith("service:jmx:rmi:")) { + throw new IOException("Unsupported JMX URL protocol. Only service:jmx:rmi: is allowed."); + } + } else { + // More strict formatting with proper escaping + String host = jmxProtocol.getHost(); + int port = Integer.parseInt(jmxProtocol.getPort()); + + // Additional validation at connection time + if (!isValidHostname(host)) { + throw new IOException("Invalid hostname format for JMX connection: " + host); + } + if (port <= 0 || port > 65535) { + throw new IOException("Invalid port for JMX connection: " + port); + } + + url = JMX_URL_PREFIX + host + ":" + port + JMX_URL_SUFFIX; + } + + // Set security properties to prevent remote class loading + System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false"); + System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase", "false"); + + Map<String, Object> environment = new HashMap<>(4); + if (StringUtils.hasText(jmxProtocol.getUsername()) && StringUtils.hasText(jmxProtocol.getPassword())) { + String[] credential = new String[] { jmxProtocol.getUsername(), jmxProtocol.getPassword() }; + environment.put(javax.management.remote.JMXConnector.CREDENTIALS, credential); + } + if (Boolean.TRUE.toString().equals(jmxProtocol.getSsl())) { + environment.put(Context.SECURITY_PROTOCOL, "ssl"); + SslRMIClientSocketFactory clientSocketFactory = new SslRMIClientSocketFactory(); + environment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientSocketFactory); + environment.put("com.sun.jndi.rmi.factory.socket", clientSocketFactory); + } + + // Limit JMX connection timeout + environment.put("jmx.remote.x.client.connection.timeout", 10000); + environment.put("jmx.remote.x.server.connection.timeout", 10000); + + try { + JMXServiceURL jmxServiceUrl = new JMXServiceURL(url); + conn = JMXConnectorFactory.connect(jmxServiceUrl, environment); + connectionCommonCache.addCache(identifier, new JmxConnect(conn)); + return conn; + } catch (Exception e) { + LogUtil.error(logger, "Failed to connect to JMX connection: {0}", e.getMessage()); + throw new IOException("Failed to connect to JMX server: " + e.getMessage(), e); + } + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/JmxValidator.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/JmxValidator.java new file mode 100644 index 0000000..a44bf07 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/JmxValidator.java @@ -0,0 +1,41 @@ +/* + * 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.collector.collect.jmx; + +/** + * If you want to customize JMX, please implement this interface + */ + +public interface JmxValidator { + /** + * Validates whether the given object name matches the validator's criteria. + * + * @param objectName The JMX object name to validate. + * @return true if valid, false otherwise. + */ + boolean isValid(String objectName); + + /** + * Retrieves the appropriate MBeanProcessor for the given object name. + * + * @param objectName The JMX object name. + * @return The corresponding MBeanProcessor instance, or null if not applicable. + */ + MbeanProcessor getProcessor(String objectName); +} + diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/MbeanProcessor.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/MbeanProcessor.java new file mode 100644 index 0000000..ce4642a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/MbeanProcessor.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.jmx; + +import java.util.Map; +import java.util.Set; + +import javax.management.MBeanServerConnection; +import javax.management.ObjectInstance; +import javax.management.ObjectName; + +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * If you want to customize JMX, please implement this interface + */ + +public interface MbeanProcessor { + + /** + * Theoretically, any customized requirement can be handled in preProcess, bypassing the general JMX collection method. + */ + void preProcess(CollectRep.MetricsData.Builder builder, Metrics metrics); + + /** + * Additional customized tasks, depending on the general JMX collection method. + */ + void process(MBeanServerConnection serverConnection, ObjectInstance objectInstance, + Set<ObjectInstance> objectInstanceSet, ObjectName objectName, Map<String, String> attributeValueMap, CollectRep.ValueRow.Builder valueRowBuilder); + + /** + * Indicator of whether the collection is complete. + */ + Boolean isCollectionComplete(); +} + diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/kafkajmx/KafkaJmxValidator.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/kafkajmx/KafkaJmxValidator.java new file mode 100644 index 0000000..b5660fe --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/kafkajmx/KafkaJmxValidator.java @@ -0,0 +1,80 @@ +/* + * 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.collector.collect.jmx.kafkajmx; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.jmx.JmxValidator; +import org.apache.hertzbeat.collector.collect.jmx.MbeanProcessor; +import org.apache.hertzbeat.collector.collect.jmx.kafkajmx.kafkaprocessor.KafkaBytesInAndOutPerSecProcessor; +import org.apache.hertzbeat.collector.collect.jmx.kafkajmx.kafkaprocessor.KafkaCommonProcessor; +import org.apache.hertzbeat.collector.collect.jmx.kafkajmx.kafkaprocessor.KafkaReplicaManageProcessor; + + + +import java.util.function.Supplier; + + +/** + * KafkaJmxValidator + */ + +public class KafkaJmxValidator implements JmxValidator { + + // Map of object name patterns to their corresponding processor suppliers + private static final Map<String, Supplier<MbeanProcessor>> OBJECT_NAME_MAP; + + static { + Map<String, Supplier<MbeanProcessor>> map = new HashMap<>(); + map.put("kafka\\.server:type=BrokerTopicMetrics,name=BytesInPerSec,topic=\\*", + KafkaBytesInAndOutPerSecProcessor::new); + map.put("kafka\\.server:type=BrokerTopicMetrics,name=BytesOutPerSec,topic=\\*", + KafkaBytesInAndOutPerSecProcessor::new); + map.put("kafka\\.server:type=ReplicaManager,name=\\*", + KafkaReplicaManageProcessor::new); + map.put("kafka\\.controller:type=KafkaController,name=\\*", + KafkaCommonProcessor::new); + map.put("kafka\\..*:type=GroupMetadataManager,name=\\*", + KafkaCommonProcessor::new); + OBJECT_NAME_MAP = Collections.unmodifiableMap(map); + } + + @Override + public boolean isValid(String objectName) { + if (StringUtils.isBlank(objectName)) { + return false; + } + return OBJECT_NAME_MAP.keySet().stream() + .anyMatch(pattern -> objectName.matches(pattern)); + } + + @Override + public MbeanProcessor getProcessor(String objectName) { + if (StringUtils.isBlank(objectName)) { + return null; + } + return OBJECT_NAME_MAP.entrySet().stream() + .filter(entry -> objectName.matches(entry.getKey())) + .map(entry -> entry.getValue().get()) + .findFirst() + .orElse(null); + } +} + diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/kafkajmx/kafkaprocessor/KafkaBytesInAndOutPerSecProcessor.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/kafkajmx/kafkaprocessor/KafkaBytesInAndOutPerSecProcessor.java new file mode 100644 index 0000000..3919c4a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/kafkajmx/kafkaprocessor/KafkaBytesInAndOutPerSecProcessor.java @@ -0,0 +1,58 @@ +/* + * 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.collector.collect.jmx.kafkajmx.kafkaprocessor; + +import java.util.Map; +import java.util.Set; + +import javax.management.MBeanServerConnection; +import javax.management.ObjectInstance; +import javax.management.ObjectName; + +import org.apache.hertzbeat.collector.collect.jmx.MbeanProcessor; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.entity.message.CollectRep.MetricsData.Builder; + +/** + * kafkaBytesInAndOutPerSecProcessor + */ +public class KafkaBytesInAndOutPerSecProcessor implements MbeanProcessor { + + Boolean completeFlag = false; + + @Override + public void preProcess(Builder builder, Metrics metrics) { + + } + + @Override + public void process(MBeanServerConnection serverConnection, + ObjectInstance objectInstance, Set<ObjectInstance> objectInstanceSet, ObjectName objectName, + Map<String, String> attributeValueMap, CollectRep.ValueRow.Builder valueRowBuilder) { + String topic = objectName.getKeyProperty("topic"); + attributeValueMap.put("topic", topic); + } + + @Override + public Boolean isCollectionComplete() { + return completeFlag; + } + +} + diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/kafkajmx/kafkaprocessor/KafkaCommonProcessor.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/kafkajmx/kafkaprocessor/KafkaCommonProcessor.java new file mode 100644 index 0000000..221f470 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/kafkajmx/kafkaprocessor/KafkaCommonProcessor.java @@ -0,0 +1,82 @@ +/* + * 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.collector.collect.jmx.kafkajmx.kafkaprocessor; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanServerConnection; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import javax.management.ReflectionException; + +import org.apache.hertzbeat.collector.collect.jmx.MbeanProcessor; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JmxProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep.MetricsData.Builder; +import org.apache.hertzbeat.common.entity.message.CollectRep.ValueRow; + +/** + * KafkaCommonProcessor + */ +public class KafkaCommonProcessor implements MbeanProcessor { + + Boolean completeFlag = false; + + @Override + public void preProcess(Builder builder, Metrics metrics) { + JmxProtocol jmx = metrics.getJmx(); + if (metrics.getJmx().getObjectName() != null + && metrics.getJmx().getObjectName().contains("GroupMetadataManager")) { + jmx.setObjectName("kafka.*:type=GroupMetadataManager,name=*"); + } + } + + @Override + public void process(MBeanServerConnection serverConnection, ObjectInstance objectInstance, + Set<ObjectInstance> objectInstanceSet, ObjectName objectName, Map<String, String> attributeValueMap, + ValueRow.Builder valueRowBuilder) { + for (ObjectInstance instance : objectInstanceSet) { + ObjectName currentObjectName = instance.getObjectName(); + try { + AttributeList attributes = serverConnection.getAttributes(currentObjectName, new String[] {"Value"}); + Object value = null; + if (attributes != null && !attributes.isEmpty()) { + Attribute attribute = (Attribute) attributes.get(0); + if (attribute != null) { + value = attribute.getValue(); + } + } + String key = currentObjectName.getKeyProperty("name"); + attributeValueMap.put("Value->" + key, String.valueOf(value)); + } catch (InstanceNotFoundException | ReflectionException | IOException e) { + throw new RuntimeException(e); + } + } + completeFlag = true; + } + + @Override + public Boolean isCollectionComplete() { + return completeFlag; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/kafkajmx/kafkaprocessor/KafkaReplicaManageProcessor.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/kafkajmx/kafkaprocessor/KafkaReplicaManageProcessor.java new file mode 100644 index 0000000..8530dd9 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/jmx/kafkajmx/kafkaprocessor/KafkaReplicaManageProcessor.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.jmx.kafkajmx.kafkaprocessor; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanServerConnection; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import javax.management.ReflectionException; + +import org.apache.hertzbeat.collector.collect.jmx.MbeanProcessor; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep.MetricsData; +import org.apache.hertzbeat.common.entity.message.CollectRep.ValueRow.Builder; + + +/** + * KafkaReplicaManageProcessor + */ +public class KafkaReplicaManageProcessor implements MbeanProcessor { + + private static final String FAILED_ISR_UPDATES_PER_SEC = "FailedIsrUpdatesPerSec"; + private static final String ISR_EXPANDS_PER_SEC = "IsrExpandsPerSec"; + private static final String ISR_SHRINKS_PER_SEC = "IsrShrinksPerSec"; + + Boolean completeFlag = false; + + @Override + public void preProcess(MetricsData.Builder builder, Metrics metrics) { + + } + + @Override + public void process(MBeanServerConnection serverConnection, ObjectInstance objectInstance, Set<ObjectInstance> objectInstanceSet, + ObjectName objectName, Map<String, String> attributeValueMap, Builder valueRowBuilder) { + for (ObjectInstance instance : objectInstanceSet) { + ObjectName currentObjectName = instance.getObjectName(); + try { + AttributeList attributes = serverConnection.getAttributes(currentObjectName, new String[] {"Value"}); + if (FAILED_ISR_UPDATES_PER_SEC.equals(currentObjectName.getKeyProperty("name")) + || ISR_EXPANDS_PER_SEC.equals(currentObjectName.getKeyProperty("name")) + || ISR_SHRINKS_PER_SEC.equals(currentObjectName.getKeyProperty("name"))) { + attributes = serverConnection.getAttributes(currentObjectName, new String[] {"Count"}); + } + Object value = null; + + if (attributes != null && !attributes.isEmpty()) { + Attribute attribute = (Attribute) attributes.get(0); + if (attribute != null) { + value = attribute.getValue(); + } + } + String key = currentObjectName.getKeyProperty("name"); + attributeValueMap.put("Value->" + key, String.valueOf(value)); + } catch (InstanceNotFoundException | ReflectionException | IOException e) { + throw new RuntimeException(e); + } + } + completeFlag = true; + } + + @Override + public Boolean isCollectionComplete() { + return completeFlag; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/memcached/MemcachedCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/memcached/MemcachedCollectImpl.java new file mode 100644 index 0000000..cbfca72 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/memcached/MemcachedCollectImpl.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.memcached; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.MemcachedProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * memcached collect + */ +@Slf4j +public class MemcachedCollectImpl extends AbstractCollect { + + private static final String STATS = "stats"; + private static final String STATS_SETTINGS = "stats settings"; + private static final String STATS_SIZES = "stats sizes"; + private static final String STATS_END_RSP = "END"; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getMemcached() == null) { + throw new IllegalArgumentException("Memcached collect must has Memcached params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + + MemcachedProtocol memcachedProtocol = metrics.getMemcached(); + String memcachedHost = memcachedProtocol.getHost(); + String memcachedPort = memcachedProtocol.getPort(); + Socket socket = null; + try { + socket = new Socket(); + SocketAddress socketAddress = new InetSocketAddress(memcachedHost, Integer.parseInt(memcachedPort)); + socket.connect(socketAddress); + if (socket.isConnected()) { + long responseTime = System.currentTimeMillis() - startTime; + PrintWriter out = new PrintWriter(socket.getOutputStream(), true); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + // Send a command to collect statistics + Map<String, String> resultMap = new HashMap<>(128); + parseCmdResponse(resultMap, in, out, STATS); + parseCmdResponse(resultMap, in, out, STATS_SETTINGS); + parseSizesOutput(resultMap, in, out); + + resultMap.put(CollectorConstants.RESPONSE_TIME, Long.toString(responseTime)); + + // Close the output stream and socket connection + in.close(); + out.close(); + socket.close(); + List<String> aliasFields = metrics.getAliasFields(); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String field : aliasFields) { + String fieldValue = resultMap.get(field); + valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } + builder.addValueRow(valueRowBuilder.build()); + } else { + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer connect failed:"); + } + } catch (UnknownHostException unknownHostException) { + String errorMsg = CommonUtil.getMessageFromThrowable(unknownHostException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("UnknownHost:" + errorMsg); + } catch (SocketTimeoutException socketTimeoutException) { + String errorMsg = CommonUtil.getMessageFromThrowable(socketTimeoutException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Socket connect timeout: " + errorMsg); + } catch (IOException ioException) { + String errorMsg = CommonUtil.getMessageFromThrowable(ioException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Connect fail:" + errorMsg); + } finally { + if (socket != null) { + try { + socket.close(); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + } + } + + private static void parseCmdResponse(Map<String, String> statsMap, + BufferedReader in, + PrintWriter out, + String cmd) throws IOException { + out.println(cmd); + String line; + while ((line = in.readLine()) != null && !line.equals(STATS_END_RSP)) { + // Parse each line and store the key-value pairs in a HashMap + String[] parts = line.split(" "); + if (parts.length == 3) { + statsMap.put(parts[1], parts[2]); + } + } + } + + private static void parseSizesOutput(Map<String, String> statsMap, + BufferedReader in, + PrintWriter out) throws IOException { + out.println(STATS_SIZES); + String line; + while ((line = in.readLine()) != null && !line.equals(STATS_END_RSP)) { + String[] parts = line.split("\\s+"); + // Extract slab size and slab count, then add them to the HashMap + if (parts.length >= 3 && "STAT".equals(parts[0])) { + statsMap.put("item_size", parts[1]); + statsMap.put("item_count", parts[2]); + } + } + } + + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_MEMCACHED; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/modbus/ModbusCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/modbus/ModbusCollectImpl.java new file mode 100644 index 0000000..1bf26c5 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/modbus/ModbusCollectImpl.java @@ -0,0 +1,86 @@ +/* + * 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.collector.collect.modbus; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.plc.AbstractPlcCollectImpl; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.ModbusProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.PlcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.plc4x.java.api.PlcConnection; +import org.apache.plc4x.java.api.messages.PlcReadRequest; +import org.springframework.beans.BeanUtils; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * plc collect + */ +@Slf4j +public class ModbusCollectImpl extends AbstractPlcCollectImpl { + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + ModbusProtocol modbus = metrics.getModbus(); + List<String> registerAddressList = modbus.getRegisterAddresses(); + // check slaveId + if (!StringUtils.hasText(modbus.getSlaveId())) { + modbus.setSlaveId("1"); + } + if (!StringUtils.hasText(modbus.getTimeout())) { + modbus.setTimeout("5000"); + } + PlcProtocol plc = metrics.getPlc() == null ? new PlcProtocol() : metrics.getPlc(); + plc.setRegisterAddresses(registerAddressList); + BeanUtils.copyProperties(modbus, plc); + metrics.setPlc(plc); + super.preCheck(metrics); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + super.collect(builder, metrics); + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_MODBUS; + } + + @Override + protected String getConnectionString(Metrics metrics) { + ModbusProtocol plcProtocol = metrics.getModbus(); + return "modbus-tcp:tcp://" + plcProtocol.getHost() + ":" + plcProtocol.getPort() + "?unit-identifier=" + plcProtocol.getSlaveId(); + } + + @Override + protected PlcReadRequest buildRequest(Metrics metrics, PlcConnection connection) { + ModbusProtocol modbus = metrics.getModbus(); + List<String> registerAddressList = modbus.getRegisterAddresses(); + // Create a new read request: + PlcReadRequest.Builder requestBuilder = connection.readRequestBuilder(); + for (int i = 0; i < registerAddressList.size(); i++) { + String s1 = modbus.getAddressSyntax() + ":" + registerAddressList.get(i); + requestBuilder.addTagAddress(metrics.getModbus().getAddressSyntax() + ":" + i, s1); + } + return requestBuilder.build(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/mqtt/CertificateFormatter.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/mqtt/CertificateFormatter.java new file mode 100644 index 0000000..312d35d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/mqtt/CertificateFormatter.java @@ -0,0 +1,195 @@ +/* + * 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.collector.collect.mqtt; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Formats the private key and certificate, supporting concatenation of multiple certificates in PEM format. + */ +public class CertificateFormatter { + + public static String formatCertificateChain(String input) { + if (input == null || input.trim().isEmpty()) { + return input; + } + + String normalized = normalizeInput(input); + + List<String> certificates = extractCertificates(normalized); + + if (certificates.isEmpty()) { + return formatAsSingleCertificate(normalized); + } + + StringBuilder formattedChain = new StringBuilder(); + for (String cert : certificates) { + if (cert.trim().isEmpty()) continue; + + String formatted = formatPemBlock(cert); + formattedChain.append(formatted).append("\n"); + } + + return formattedChain.toString().trim(); + } + + private static String normalizeInput(String input) { + return input + .replace("\r\n", "\n") + .replace("\r", "\n") + .replaceAll("\\s*\\\\n\\s*", "\n") + .replaceAll("(?m)^\\s+|\\s+$", "") + .trim(); + } + + private static List<String> extractCertificates(String input) { + List<String> certificates = new ArrayList<>(); + String regex = "(-----BEGIN\\s+[\\w\\s]+?-----)[\\s\\S]*?(-----END\\s+[\\w\\s]+?-----)"; + + + Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(input); + + int lastEnd = 0; + while (matcher.find()) { + + if (matcher.start() > lastEnd) { + String gap = input.substring(lastEnd, matcher.start()); + if (!gap.trim().isEmpty()) { + certificates.add(gap); + } + } + + certificates.add(matcher.group()); + lastEnd = matcher.end(); + } + + + if (lastEnd < input.length()) { + certificates.add(input.substring(lastEnd)); + } + + return certificates; + } + + private static String formatPemBlock(String block) { + try { + Pattern pattern = Pattern.compile( + "(-----BEGIN\\s+[\\w\\s]+?-----)(.*?)(-----END\\s+[\\w\\s]+?-----)", + Pattern.DOTALL | Pattern.CASE_INSENSITIVE + ); + + Matcher matcher = pattern.matcher(block); + if (matcher.find()) { + String header = matcher.group(1).trim(); + String body = matcher.group(2); + String footer = matcher.group(3).trim(); + + + if (body == null) body = ""; + + String cleanBody = body + .replaceAll("\\s", "") + .replaceAll("\"", "") + .trim(); + + + if (cleanBody.isEmpty() && body != null && !body.trim().isEmpty()) { + + cleanBody = body.replaceAll("[^a-zA-Z0-9+/=]", "").trim(); + } + + String formattedBody = formatBase64Body(cleanBody); + + return header + "\n" + formattedBody + "\n" + footer; + } else { + + return formatAsCertificate(block); + } + } catch (Exception e) { + + return block; + } + } + + private static String formatAsCertificate(String content) { + + String cleanContent = content.replaceAll("[^a-zA-Z0-9+/=]", "").trim(); + + if (cleanContent.isEmpty()) { + return content; + } + + + String formattedBody = formatBase64Body(cleanContent); + + + if (cleanContent.toLowerCase().contains("private")) { + if (cleanContent.startsWith("MII") || cleanContent.length() > 1000) { + return "-----BEGIN PRIVATE KEY-----\n" + formattedBody + "\n-----END PRIVATE KEY-----"; + } else { + return "-----BEGIN RSA PRIVATE KEY-----\n" + formattedBody + "\n-----END RSA PRIVATE KEY-----"; + } + } else { + return "-----BEGIN CERTIFICATE-----\n" + formattedBody + "\n-----END CERTIFICATE-----"; + } + } + + private static String formatAsSingleCertificate(String input) { + String cleanContent = input.replaceAll("[^a-zA-Z0-9+/=]", "").trim(); + return formatAsCertificate(cleanContent); + } + + private static String formatBase64Body(String body) { + + StringBuilder formatted = new StringBuilder(); + int index = 0; + while (index < body.length()) { + int end = Math.min(index + 64, body.length()); + formatted.append(body.substring(index, end)); + if (end < body.length()) { + formatted.append("\n"); + } + index = end; + } + return formatted.toString().trim(); + } + + public static String formatPrivateKey(String input) { + if (input == null || input.trim().isEmpty()) { + return input; + } + + + String normalized = normalizeInput(input); + + + if (isPemEncapsulated(normalized)) { + return formatPemBlock(normalized); + } + + return formatAsCertificate(normalized); + } + + private static boolean isPemEncapsulated(String block) { + return block.contains("-----BEGIN") && block.contains("-----END"); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/mqtt/MqttCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/mqtt/MqttCollectImpl.java new file mode 100644 index 0000000..befd864 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/mqtt/MqttCollectImpl.java @@ -0,0 +1,291 @@ +/* + * 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.collector.collect.mqtt; + + +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.MqttProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.entity.message.CollectRep.MetricsData.Builder; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttClientPersistence; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; +import org.springframework.util.StopWatch; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * collect mqtt metrics using Eclipse Paho + */ +public class MqttCollectImpl extends AbstractCollect { + + public MqttCollectImpl() { + } + + private static final Logger logger = LoggerFactory.getLogger(MqttCollectImpl.class); + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_MQTT; + } + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + MqttProtocol mqttProtocol = metrics.getMqtt(); + Assert.hasText(mqttProtocol.getHost(), "MQTT protocol host is required"); + Assert.hasText(mqttProtocol.getPort(), "MQTT protocol port is required"); + + if ("mqtts".equalsIgnoreCase(mqttProtocol.getProtocol())) { + if (Boolean.parseBoolean(mqttProtocol.getEnableMutualAuth())) { + Assert.hasText(mqttProtocol.getCaCert(), "CA certificate is required for mutual auth"); + Assert.hasText(mqttProtocol.getClientCert(), "Client certificate is required for mutual auth"); + Assert.hasText(mqttProtocol.getClientKey(), "Client private key is required for mutual auth"); + } + } + } + + @Override + public void collect(Builder builder, Metrics metrics) { + MqttProtocol mqttProtocol = metrics.getMqtt(); + Map<Object, String> data = new HashMap<>(); + + try { + MqttAsyncClient client = buildMqttClient(mqttProtocol); + long responseTime = connectClient(client, mqttProtocol); + testSubscribeAndPublish(client, mqttProtocol, data); + convertToMetricsData(builder, metrics, responseTime, data); + client.disconnect(); + } catch (Exception e) { + logger.error("MQTT collection error: {}", e.getMessage(), e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Collection failed: " + e.getMessage()); + } + } + + private MqttAsyncClient buildMqttClient(MqttProtocol protocol) throws Exception { + String clientId = protocol.getClientId(); + + String serverUri = String.format("%s://%s:%s", + StringUtils.equals(protocol.getProtocol(), "MQTT") ? "tcp" : "ssl", + protocol.getHost(), + protocol.getPort()); + + MqttClientPersistence persistence = new MemoryPersistence(); + + return new MqttAsyncClient(serverUri, clientId, persistence); + } + + private long connectClient(MqttAsyncClient client, MqttProtocol protocol) throws Exception { + MqttConnectOptions connOpts = new MqttConnectOptions(); + + if (protocol.hasAuth()) { + connOpts.setUserName(protocol.getUsername()); + connOpts.setPassword(protocol.getPassword().toCharArray()); + } + + connOpts.setKeepAliveInterval(Integer.parseInt(protocol.getKeepalive())); + connOpts.setConnectionTimeout(Integer.parseInt(protocol.getTimeout()) / 1000); + connOpts.setCleanSession(true); + connOpts.setAutomaticReconnect(false); + if ("mqtts".equalsIgnoreCase(protocol.getProtocol())) { + boolean insecureSkipVerify = Boolean.parseBoolean(protocol.getInsecureSkipVerify()); + if (insecureSkipVerify) { + connOpts.setHttpsHostnameVerificationEnabled(false); + } + if (Boolean.parseBoolean(protocol.getEnableMutualAuth())) { + connOpts.setSocketFactory(MqttSslFactory.getMslSocketFactory(protocol, insecureSkipVerify)); + } else { + connOpts.setSocketFactory(MqttSslFactory.getSslSocketFactory(protocol, insecureSkipVerify)); + } + } + + + StopWatch connectWatch = new StopWatch(); + connectWatch.start(); + + client.connect(connOpts).waitForCompletion(Long.parseLong(protocol.getTimeout())); + connectWatch.stop(); + return connectWatch.getTotalTimeMillis(); + } + + + /** + * Test MQTT subscribe and publish capabilities + */ + private void testSubscribeAndPublish(MqttAsyncClient client, MqttProtocol protocol, Map<Object, String> data) { + + // 1 test subscribe + if (StringUtils.isNotBlank(protocol.getTopic())) { + String subscribe = testSubscribe(client, protocol.getTopic()); + if (StringUtils.isBlank(subscribe)) { + data.put("canSubscribe", "Subscription successful"); + } else { + data.put("canSubscribe", String.format("Subscription failed: %s", subscribe)); + } + + } else { + data.put("canSubscribe", "No topic, subscription test skipped"); + } + + + // 2 test publish + if (StringUtils.isNotBlank(protocol.getTestMessage())) { + String publish = testPublish(client, protocol.getTopic(), protocol.getTestMessage()); + if (StringUtils.isBlank(publish)) { + data.put("canPublish", "Message published successfully"); + + // 3 test receive message + String receivedData = getReceivedData(client, protocol.getTopic()); + data.put("canReceive", receivedData); + } else { + data.put("canPublish", String.format("Message publishing failed: %s", publish)); + data.put("canReceive", "Message reception skipped due to failed publish"); + } + } else { + data.put("canPublish", "No test message, publish test skipped"); + data.put("canReceive", "No test message, receive test skipped"); + } + + + // 4 test unsubscribe + if (StringUtils.isNotBlank(protocol.getTopic())) { + String subscribe = testUnSubscribe(client, protocol.getTopic()); + if (StringUtils.isBlank(subscribe)) { + data.put("canUnSubscribe", "Unsubscription successful"); + } else { + data.put("canUnSubscribe", String.format("Unsubscription failed: %s", subscribe)); + } + } else { + data.put("canUnSubscribe", "No topic, unsubscription test skipped"); + } + } + + private String getReceivedData(MqttAsyncClient client, String topic) { + final CountDownLatch latch = new CountDownLatch(1); + final StringBuilder messageHolder = new StringBuilder(); + + + client.setCallback(new MqttCallback() { + @Override + public void connectionLost(Throwable cause) { + latch.countDown(); + } + + @Override + public void messageArrived(String arrivedTopic, MqttMessage message) { + + if (topic.equals(arrivedTopic)) { + messageHolder.append(new String(message.getPayload())); + latch.countDown(); + } + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) { + } + }); + + try { + boolean received = latch.await(5, TimeUnit.SECONDS); + if (messageHolder.length() > 0) { + return messageHolder.toString(); + } else if (!received) { + return "Message reception timed out after 5 seconds"; + } else { + return "No valid message received"; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return e.getMessage(); + } finally { + client.setCallback(null); + } + } + + private String testSubscribe(MqttAsyncClient client, String topic) { + try { + IMqttToken subToken = client.subscribe(topic, 1); + subToken.waitForCompletion(5000); + return ""; + } catch (MqttException e) { + logger.warn("MQTT subscribe test failed: {}", e.getMessage()); + return e.getMessage(); + } + } + + private String testPublish(MqttAsyncClient client, String topic, String message) { + try { + MqttMessage mqttMessage = new MqttMessage(message.getBytes()); + mqttMessage.setQos(1); + + IMqttToken pubToken = client.publish(topic, mqttMessage); + pubToken.waitForCompletion(5000); + + return ""; + } catch (MqttException e) { + logger.warn("MQTT publish test failed: {}", e.getMessage()); + return e.getMessage(); + } + } + + private String testUnSubscribe(MqttAsyncClient client, String topic) { + try { + IMqttToken unsubToken = client.unsubscribe(topic); + unsubToken.waitForCompletion(5000); + return ""; + } catch (MqttException e) { + logger.warn("MQTT unsubscribe test failed: {}", e.getMessage()); + return e.getMessage(); + } + } + + /** + * Convert collected data to MetricsData + */ + private void convertToMetricsData(Builder builder, Metrics metrics, long responseTime, Map<Object, String> data) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String column : metrics.getAliasFields()) { + if (CollectorConstants.RESPONSE_TIME.equals(column)) { + valueRowBuilder.addColumn(String.valueOf(responseTime)); + } else { + String value = data.get(column); + value = value == null ? CommonConstants.NULL_VALUE : value; + valueRowBuilder.addColumn(value); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/mqtt/MqttSslFactory.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/mqtt/MqttSslFactory.java new file mode 100644 index 0000000..3c78bf8 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/mqtt/MqttSslFactory.java @@ -0,0 +1,186 @@ +/* + * 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.collector.collect.mqtt; + +import org.apache.hertzbeat.common.entity.job.protocol.MqttProtocol; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; + +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.StringReader; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collection; + +/** + * Support MQTT SSL Factory + */ +public class MqttSslFactory { + + /** + * Get MSL Socket Factory + */ + public static SSLSocketFactory getMslSocketFactory(MqttProtocol mqttProtocol, boolean insecureSkipVerify) { + try { + Security.addProvider(new BouncyCastleProvider()); + + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null, null); + + Certificate[] chain = null; + if (mqttProtocol.getClientCert() != null && !mqttProtocol.getClientCert().isEmpty()) { + String formatClientCert = CertificateFormatter.formatCertificateChain(mqttProtocol.getClientCert()); + try (InputStream certIn = new ByteArrayInputStream(formatClientCert.getBytes())) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Collection<? extends Certificate> certs = cf.generateCertificates(certIn); + chain = certs.toArray(new Certificate[0]); + } + } + + PrivateKey privateKey; + if (mqttProtocol.getClientKey() != null && !mqttProtocol.getClientKey().isEmpty()) { + String formatClientKey = CertificateFormatter.formatPrivateKey(mqttProtocol.getClientKey()); + try (PEMParser pemParser = new PEMParser(new StringReader(formatClientKey))) { + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + Object object = pemParser.readObject(); + + if (object instanceof PEMKeyPair) { + privateKey = converter.getPrivateKey(((PEMKeyPair) object).getPrivateKeyInfo()); + } else if (object instanceof PrivateKeyInfo) { + privateKey = converter.getPrivateKey((PrivateKeyInfo) object); + } else { + throw new IllegalArgumentException("Unsupported private key type"); + } + + ks.setKeyEntry("private-key", privateKey, "".toCharArray(), chain); + } + } + + TrustManager[] trustManagers; + if (insecureSkipVerify) { + trustManagers = createInsecureTrustManager(); + } else { + String formatCaCert = CertificateFormatter.formatCertificateChain(mqttProtocol.getCaCert()); + KeyStore trustStore = createMergedTrustStore(formatCaCert); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(trustStore); + trustManagers = tmf.getTrustManagers(); + } + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, "".toCharArray()); + + + SSLContext context = SSLContext.getInstance(mqttProtocol.getTlsVersion()); + context.init(kmf.getKeyManagers(), trustManagers, null); + + return context.getSocketFactory(); + } catch (Exception e) { + throw new RuntimeException("Fails to SSL initialize: " + e.getMessage(), e); + } + } + + /** + * Get SSL Socket Factory + */ + public static SSLSocketFactory getSslSocketFactory(MqttProtocol mqttProtocol, boolean insecureSkipVerify) { + try { + Security.addProvider(new BouncyCastleProvider()); + + + TrustManager[] trustManagers; + if (insecureSkipVerify) { + trustManagers = createInsecureTrustManager(); + } else { + + String formatCaCert = CertificateFormatter.formatCertificateChain(mqttProtocol.getCaCert()); + KeyStore trustStore = createMergedTrustStore(formatCaCert); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(trustStore); + trustManagers = tmf.getTrustManagers(); + } + + SSLContext sslContext = SSLContext.getInstance(mqttProtocol.getTlsVersion()); + sslContext.init(null, trustManagers, null); + + return sslContext.getSocketFactory(); + } catch (Exception e) { + throw new RuntimeException("Fails to SSL initialize: " + e.getMessage(), e); + } + } + + private static TrustManager[] createInsecureTrustManager() { + return new TrustManager[]{ + new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + }; + } + + private static KeyStore createMergedTrustStore(String caCertPem) throws Exception { + KeyStore mergedKs = KeyStore.getInstance(KeyStore.getDefaultType()); + mergedKs.load(null, null); + + + TrustManagerFactory systemTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + systemTmf.init((KeyStore) null); + X509TrustManager systemTm = (X509TrustManager) systemTmf.getTrustManagers()[0]; + + int systemIndex = 1; + for (X509Certificate cert : systemTm.getAcceptedIssuers()) { + mergedKs.setCertificateEntry("system-ca-" + systemIndex++, cert); + } + + + if (caCertPem != null && !caCertPem.isEmpty()) { + try (InputStream caIn = new ByteArrayInputStream(caCertPem.getBytes())) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Collection<? extends Certificate> customCerts = cf.generateCertificates(caIn); + + int customIndex = 1; + for (Certificate cert : customCerts) { + mergedKs.setCertificateEntry("custom-ca-" + customIndex++, cert); + } + } + } + + return mergedKs; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/nginx/NginxCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/nginx/NginxCollectImpl.java new file mode 100644 index 0000000..9fa5783 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/nginx/NginxCollectImpl.java @@ -0,0 +1,338 @@ +/* + * 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.collector.collect.nginx; + +import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.NginxProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpHost; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.springframework.http.MediaType; + +/** + * nginx collect + */ +@Slf4j +public class NginxCollectImpl extends AbstractCollect { + + private static final String NGINX_STATUS_NAME = "nginx_status"; + private static final String REQ_STATUS_NAME = "req_status"; + private static final String AVAILABLE = "available"; + private static final String CONNECTIONS = "connections"; + private static final String ACTIVE = "active"; + private static final String GET = "get"; + private static final String FIELD_SPLIT = "_"; + private static final String REGEX_KEYS = "server\\s+(\\w+)\\s+(\\w+)\\s+(\\w+)"; + private static final String REGEX_VALUES = "(\\d+) (\\d+) (\\d+)"; + private static final String REGEX_SERVER = "(\\w+): (\\d+)"; + private static final String REGEX_SPLIT = "\\r?\\n"; + private static final String REGEX_LINE_SPLIT = "\\s+"; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + final NginxProtocol nginxProtocol; + if (metrics == null || (nginxProtocol = metrics.getNginx()) == null || nginxProtocol.isInvalid()) { + throw new IllegalArgumentException("Nginx collect must has nginx params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + + NginxProtocol nginxProtocol = metrics.getNginx(); + String url = nginxProtocol.getUrl(); + if (StringUtils.isEmpty(url) || !url.startsWith(RIGHT_DASH)) { + nginxProtocol.setUrl(url == null ? RIGHT_DASH : RIGHT_DASH + url.trim()); + } + + HttpContext httpContext = createHttpContext(metrics.getNginx()); + HttpUriRequest request = createHttpRequest(metrics.getNginx()); + try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)){ + // send an HTTP request and get the response data + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != HttpStatus.SC_OK) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(NetworkConstants.STATUS_CODE + statusCode); + return; + } + String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + + Long responseTime = System.currentTimeMillis() - startTime; + // call different parsing methods based on the metrics name + if (StringUtils.equalsAny(metrics.getName(), NGINX_STATUS_NAME, AVAILABLE)) { + parseNginxStatusResponse(builder, resp, metrics, responseTime); + } else if (REQ_STATUS_NAME.equals(metrics.getName())) { + parseReqStatusResponse(builder, resp, metrics, responseTime); + } + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.info(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (request != null) { + request.abort(); + } + } + + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_NGINX; + } + + private HttpContext createHttpContext(NginxProtocol nginxProtocol) { + HttpHost host = new HttpHost(nginxProtocol.getHost(), Integer.parseInt(nginxProtocol.getPort())); + HttpClientContext httpClientContext = new HttpClientContext(); + httpClientContext.setTargetHost(host); + return httpClientContext; + } + + private HttpUriRequest createHttpRequest(NginxProtocol nginxProtocol) { + RequestBuilder requestBuilder = RequestBuilder.get(); + String portWithUri = nginxProtocol.getPort() + CollectUtil.replaceUriSpecialChar(nginxProtocol.getUrl()); + String host = nginxProtocol.getHost(); + + if (IpDomainUtil.isHasSchema(host)) { + requestBuilder.setUri(host + ":" + portWithUri); + } else { + String ipAddressType = IpDomainUtil.checkIpAddressType(host); + String baseUri = NetworkConstants.IPV6.equals(ipAddressType) + ? String.format("[%s]:%s", host, portWithUri) + : String.format("%s:%s", host, portWithUri); + + boolean ssl = Boolean.parseBoolean(nginxProtocol.getSsl()); + if (ssl){ + requestBuilder.setUri(NetworkConstants.HTTPS_HEADER + baseUri); + } else { + requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri); + } + } + + requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE); + requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT); + requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE); + + int timeout = Integer.parseInt(nginxProtocol.getTimeout()); + if (timeout > 0) { + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(timeout) + .setSocketTimeout(timeout) + .setRedirectsEnabled(true) + .build(); + requestBuilder.setConfig(requestConfig); + } + return requestBuilder.build(); + } + + /** + * analyze the information exposed by nginx's built-in ngx_http_stub_status_module + * + * @param builder builder + * @param resp resp + * @param metrics metrics + * @param responseTime responseTime + */ + private void parseNginxStatusResponse(CollectRep.MetricsData.Builder builder, String resp, Metrics metrics, + Long responseTime) { + //example + //Active connections: 2 + //server accepts handled requests + //4 4 2 + //Reading: 0 Writing: 1 Waiting: 1 + List<String> aliasFields = metrics.getAliasFields(); + Map<String, Object> metricMap = regexNginxStatusMatch(resp, metrics.getAliasFields().size()); + // Returned data + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + Object value = metricMap.get(alias); + if (value != null) { + valueRowBuilder.addColumn(String.valueOf(value)); + } else { + if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(responseTime.toString()); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + } + builder.addValueRow(valueRowBuilder.build()); + } + + /** + * Analyze the information exposed by the ngx_http_reqstat_module module + * + * @param builder builder + * @param resp resp + * @param metrics metrics + * @param responseTime responseTime + */ + private void parseReqStatusResponse(CollectRep.MetricsData.Builder builder, String resp, Metrics metrics, + Long responseTime) { + //example + //zone_name key max_active max_bw traffic requests active bandwidth + //imgstore_appid 43 27 6M 63G 374063 0 0 + //imgstore_appid 53 329 87M 2058G 7870529 50 25M + //server_addr 10.128.1.17 2 8968 24M 1849 0 0 + //server_addr 127.0.0.1 1 6M 5G 912 1 0 + //server_addr 180.96.x.1 3358 934M 27550G 141277391 891 356M + //server_addr 180.96.x.2 78 45M 220G 400704 0 0 + //server_addr 180.96.x.3 242 58M 646G 2990547 42 7M + //server_name d.123.sogou.com 478 115M 2850G 30218726 115 39M + //server_name dl.pinyin.sogou.com 913 312M 8930G 35345453 225 97M + //server_name download.ie.sogou.com 964 275M 7462G 7979817 297 135M + List<ReqStatusResponse> reqStatusResponses = regexReqStatusMatch(resp); + List<String> aliasFields = metrics.getAliasFields(); + + for (ReqStatusResponse reqStatusResponse : reqStatusResponses) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (NetworkConstants.RESPONSE_TIME.equals(alias)) { + valueRowBuilder.addColumn(String.valueOf(responseTime)); + } else { + try { + String methodName = reqStatusResponse.getFieldMethodName(alias); + Object value = reflect(reqStatusResponse, methodName); + value = value == null ? CommonConstants.NULL_VALUE : value; + valueRowBuilder.addColumn(String.valueOf(value)); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + + private Object reflect(ReqStatusResponse reqStatusResponse, String methodName) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class<?> clazz = reqStatusResponse.getClass(); + Method method = clazz.getMethod(methodName); + return method.invoke(reqStatusResponse); + } + + private Map<String, Object> regexNginxStatusMatch(String resp, Integer aliasFieldsSize) { + Map<String, Object> metricsMap = new HashMap<>(aliasFieldsSize); + // Extract monitoring information using regular expressions + Pattern pattern = Pattern.compile(REGEX_SERVER); + Matcher matcher = pattern.matcher(resp); + while (matcher.find()) { + String key = StringUtils.lowerCase(matcher.group(1)); + String value = matcher.group(2); + metricsMap.put(CONNECTIONS.equals(key) ? ACTIVE : key, value); + } + Pattern pattern1 = Pattern.compile(REGEX_KEYS); + Matcher matcher1 = pattern1.matcher(resp); + Pattern pattern2 = Pattern.compile(REGEX_VALUES); + Matcher matcher2 = pattern2.matcher(resp); + if (matcher1.find() && matcher2.find()) { + for (int i = 0; i < matcher1.groupCount(); i++) { + metricsMap.put(matcher1.group(i + 1), matcher2.group(i + 1)); + } + } + return metricsMap; + } + + private List<ReqStatusResponse> regexReqStatusMatch(String resp) { + List<ReqStatusResponse> reqStatusResponses = new ArrayList<>(); + + String[] lines = resp.split(REGEX_SPLIT); + for (int i = 1; i < lines.length; i++) { + String[] values = lines[i].split(REGEX_LINE_SPLIT); + ReqStatusResponse reqStatusResponse = ReqStatusResponse.builder() + .zoneName(values[0]) + .key(values[1]) + .maxActive(values[2]) + .maxBw(values[3]) + .traffic(values[4]) + .requests(values[5]) + .active(values[6]) + .bandwidth(values[7]) + .build(); + reqStatusResponses.add(reqStatusResponse); + } + return reqStatusResponses; + } + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + static class ReqStatusResponse { + private String zoneName; // zone_name + + private String maxActive; // max_active + + private String key; // key + + private String maxBw; // max_bw + + private String traffic; // traffic + + private String requests; // requests + + private String active; // active + + private String bandwidth; // bandwidth + + public String getFieldMethodName(String name) { + String[] words = name.split(FIELD_SPLIT); + StringBuilder result = new StringBuilder(); + for (String word : words) { + result.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1)); + } + return GET + result; + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ntp/NtpCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ntp/NtpCollectImpl.java new file mode 100644 index 0000000..bb7cc46 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ntp/NtpCollectImpl.java @@ -0,0 +1,144 @@ +/* + * 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.collector.collect.ntp; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.net.ntp.NTPUDPClient; +import org.apache.commons.net.ntp.NtpV3Packet; +import org.apache.commons.net.ntp.TimeInfo; +import org.apache.commons.net.ntp.TimeStamp; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.NtpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * ntp collect + */ +@Slf4j +public class NtpCollectImpl extends AbstractCollect { + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getNtp() == null) { + throw new IllegalArgumentException("NTP collect must have NTP params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + NtpProtocol ntpProtocol = metrics.getNtp(); + String host = ntpProtocol.getHost(); + int timeout = CollectUtil.getTimeout(ntpProtocol.getTimeout()); + + NTPUDPClient client = null; + + try { + client = new NTPUDPClient(); + client.setDefaultTimeout(timeout); + client.open(); + InetAddress serverAddress = InetAddress.getByName(host); + TimeInfo timeInfo = client.getTime(serverAddress); + long responseTime = System.currentTimeMillis() - startTime; + + timeInfo.computeDetails(); + + // Obtain NTP server information + Map<String, String> resultMap = getNtpInfo(timeInfo); + resultMap.put(CollectorConstants.RESPONSE_TIME, Long.toString(responseTime)); + + List<String> aliasFields = metrics.getAliasFields(); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String field : aliasFields) { + String fieldValue = resultMap.get(field); + valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } + builder.addValueRow(valueRowBuilder.build()); + client.close(); + } catch (SocketException socketException) { + String errorMsg = CommonUtil.getMessageFromThrowable(socketException); + log.debug(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("NTPUDPClient open is fail: " + errorMsg); + } catch (UnknownHostException unknownHostException) { + String errorMsg = CommonUtil.getMessageFromThrowable(unknownHostException); + log.debug(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("NTPServerAddress is unknownHost: " + errorMsg); + } catch (IOException ioException) { + String errorMsg = CommonUtil.getMessageFromThrowable(ioException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Receive timed out: " + timeout + "ms"); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn(errorMsg, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (client != null) { + try { + client.close(); + } catch (Exception e) { + log.warn(e.getMessage()); + } + } + } + } + + private Map<String, String> getNtpInfo(TimeInfo timeInfo) { + Map<String, String> valueMap = new HashMap<>(16); + + NtpV3Packet message = timeInfo.getMessage(); + TimeStamp timeStamp = message.getTransmitTimeStamp(); + Date date = timeStamp.getDate(); + + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); + valueMap.put("time", Long.toString(timeStamp.getTime())); + valueMap.put("date", simpleDateFormat.format(date)); + valueMap.put("offset", Long.toString(timeInfo.getOffset())); + valueMap.put("delay", Long.toString(timeInfo.getDelay())); + valueMap.put("version", Integer.toString(message.getVersion())); + valueMap.put("mode", Integer.toString(message.getMode())); + valueMap.put("stratum", Integer.toString(message.getStratum())); + valueMap.put("referenceId", String.valueOf(message.getReferenceId())); + valueMap.put("precision", Integer.toString(message.getPrecision())); + return valueMap; + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_NTP; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/plc/AbstractPlcCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/plc/AbstractPlcCollectImpl.java new file mode 100644 index 0000000..6e45a1d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/plc/AbstractPlcCollectImpl.java @@ -0,0 +1,185 @@ +/* + * 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.collector.collect.plc; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.PlcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.plc4x.java.api.PlcConnection; +import org.apache.plc4x.java.api.PlcConnectionManager; +import org.apache.plc4x.java.api.PlcDriverManager; +import org.apache.plc4x.java.api.messages.PlcReadRequest; +import org.apache.plc4x.java.api.messages.PlcReadResponse; +import org.apache.plc4x.java.api.types.PlcResponseCode; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +/** + * abstract plc collect implement + */ +@Slf4j +public abstract class AbstractPlcCollectImpl extends AbstractCollect { + private static final String[] DRIVER_LIST = {"s7", "modbus-tcp"}; + private static final String[] ADDRESS_SYNTAX = {"discrete-input", "coil", "input-register", "holding-register"}; + private static final String COIL = "coil"; + + private static final PlcConnectionManager CONNECTION_MANAGER; + + static { + CONNECTION_MANAGER = PlcDriverManager.getDefault().getConnectionManager(); + } + + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getPlc() == null) { + throw new IllegalArgumentException("PLC collect must have PLC params"); + } + // check driver name + if (metrics.getPlc().getDriverName() == null || !ArrayUtils.contains(DRIVER_LIST, metrics.getPlc().getDriverName())) { + throw new IllegalArgumentException("PLC collect must have valid driver name"); + } + // check address syntax + if (!ArrayUtils.contains(ADDRESS_SYNTAX, metrics.getPlc().getAddressSyntax())) { + throw new IllegalArgumentException("PLC collect must have valid address syntax"); + } + // check register address + if (metrics.getPlc().getRegisterAddresses() == null || metrics.getPlc().getRegisterAddresses().isEmpty()) { + throw new IllegalArgumentException("PLC collect must have register address"); + } + // check timeout is legal + if (Objects.nonNull(metrics.getPlc().getTimeout())) { + try { + Long.parseLong(metrics.getPlc().getTimeout()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("PLC collect must have valid timeout"); + } + } + + AtomicInteger addressCount = new AtomicInteger(); + metrics.getPlc().getRegisterAddresses().forEach(address -> { + if (address.contains("[") && address.contains("]")) { + String[] addressArray = address.split("\\["); + String num = addressArray[1].replace("]", ""); + addressCount.addAndGet(Integer.parseInt(num)); + } else { + addressCount.addAndGet(1); + } + }); + List<String> aliasFields = metrics.getAliasFields(); + if (Objects.isNull(aliasFields)) { + throw new IllegalArgumentException("Please ensure that the number of aliasFields (tagName) in yml matches the number of registered addresses" + + "Number of AliasFields(tagList): 0 ,but Number of addresses:" + + addressCount.get()); + } + int tagListCount = aliasFields.size() - 1; + if (aliasFields.size() - 1 != addressCount.get()) { + throw new IllegalArgumentException("Please ensure that the number of aliasFields (tagName) in yml matches the number of registered addresses" + + "Number of AliasFields(tagList): " + + tagListCount + + " ,but Number of addresses:" + addressCount.get()); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + + long startTime = System.currentTimeMillis(); + PlcProtocol plcProtocol = metrics.getPlc(); + PlcConnection plcConnection = null; + try { + String connectionString = getConnectionString(metrics); + plcConnection = CONNECTION_MANAGER.getConnection(connectionString); + if (!plcConnection.getMetadata().isReadSupported()) { + log.error("This connection doesn't support reading."); + } + // Check if this connection support reading of data. + if (!plcConnection.getMetadata().isWriteSupported()) { + log.error("This connection doesn't support writing."); + } + + PlcReadRequest readRequest = buildRequest(metrics, plcConnection); + PlcReadResponse response = readRequest.execute().get(Long.parseLong(plcProtocol.getTimeout()), TimeUnit.MILLISECONDS); + long responseTime = System.currentTimeMillis() - startTime; + Map<String, String> resultMap = new HashMap<>(); + for (String tagName : response.getTagNames()) { + if (response.getResponseCode(tagName) == PlcResponseCode.OK) { + int numValues = response.getNumberOfValues(tagName); + // If it's just one element, output just one single line. + log.info("{}: {}", tagName, response.getPlcValue(tagName)); + if (numValues == 1) { + resultMap.put(tagName, response.getPlcValue(tagName).toString()); + } + // If it's more than one element, output each in a single row. + else { + for (int i = 0; i < numValues; i++) { + resultMap.put(tagName + "-" + i, response.getObject(tagName, i).toString()); + } + } + } else { + log.error("Error[{}]: {}", tagName, response.getResponseCode(tagName).name()); + } + } + if (COIL.equals(plcProtocol.getAddressSyntax())) { + resultMap = resultMap.entrySet() + .stream() + .peek(obj -> obj.setValue(String.valueOf(Boolean.TRUE.equals(Boolean.valueOf(obj.getValue())) ? 1 : 0))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + } + resultMap.put(CollectorConstants.RESPONSE_TIME, Long.toString(responseTime)); + List<String> aliasFields = metrics.getAliasFields(); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String field : aliasFields) { + String fieldValue = resultMap.get(field); + valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } + builder.addValueRow(valueRowBuilder.build()); + } catch (Exception e) { + builder.setCode(CollectRep.Code.FAIL); + String message = CommonUtil.getMessageFromThrowable(e); + builder.setMsg(message); + log.warn(message, e); + } finally { + if (plcConnection != null) { + try { + plcConnection.close(); + } catch (Exception e) { + log.warn(e.getMessage()); + } + } + } + + } + + protected abstract String getConnectionString(Metrics metrics); + + protected abstract PlcReadRequest buildRequest(Metrics metrics, PlcConnection connection); +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/pop3/Pop3CollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/pop3/Pop3CollectImpl.java new file mode 100644 index 0000000..b0202f3 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/pop3/Pop3CollectImpl.java @@ -0,0 +1,175 @@ +/* + * 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.collector.collect.pop3; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.net.pop3.POP3Client; +import org.apache.commons.net.pop3.POP3MessageInfo; +import org.apache.commons.net.pop3.POP3SClient; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.Pop3Protocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * pop3 collect + */ +@Slf4j +public class Pop3CollectImpl extends AbstractCollect { + + private static final String EMAIL_COUNT = "email_count"; + private static final String MAILBOX_SIZE = "mailbox_size"; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + Pop3Protocol pop3Protocol; + if (metrics == null || (pop3Protocol = metrics.getPop3()) == null || pop3Protocol.isInvalid()) { + throw new IllegalArgumentException("Pop3 collect must has pop3 params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + + Pop3Protocol pop3Protocol = metrics.getPop3(); + POP3Client pop3Client = null; + boolean ssl = Boolean.parseBoolean(pop3Protocol.getSsl()); + try { + pop3Client = createPOP3Client(pop3Protocol, ssl); + + if (pop3Client.isConnected()) { + long responseTime = System.currentTimeMillis() - startTime; + + obtainPop3Metrics(builder, pop3Client, metrics.getAliasFields(), + responseTime); + } else { + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer connect failed,Timeout " + pop3Protocol.getTimeout() + "ms"); + } + } catch (Exception e2) { + String errorMsg = CommonUtil.getMessageFromThrowable(e2); + log.info(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (pop3Client != null) { + try { + pop3Client.logout(); + pop3Client.disconnect(); + } catch (IOException e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.info(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_POP3; + } + + /** + * create a POP3 connection【 with SSL encryption support 】 + * @param pop3Protocol pop3 Protocol + * @param ssl ssl + * @return return + * @throws IOException IO Exception + */ + private POP3Client createPOP3Client(Pop3Protocol pop3Protocol, boolean ssl) throws Exception { + POP3Client pop3Client = null; + // determine whether to use SSL-encrypted connections + if (ssl) { + pop3Client = new POP3SClient(true); + } else { + pop3Client = new POP3Client(); + } + // set timeout + int timeout = Integer.parseInt(pop3Protocol.getTimeout()); + if (timeout > 0) { + pop3Client.setConnectTimeout(timeout); + } + pop3Client.setCharset(StandardCharsets.UTF_8); + // connect to the POP3 server + String host = pop3Protocol.getHost(); + int port = Integer.parseInt(pop3Protocol.getPort()); + pop3Client.connect(host, port); + // validate credentials + String email = pop3Protocol.getEmail(); + String authorize = pop3Protocol.getAuthorize(); + boolean isAuthenticated = pop3Client.login(email, authorize); + if (!isAuthenticated) { + throw new Exception("Pop3 client authentication failed"); + } + return pop3Client; + } + + /** + * retrieve Pop3 metric information + * @param builder builder + * @param pop3Client pop3 client + * @param aliasFields alias Fields + * @param responseTime response Time + */ + private void obtainPop3Metrics(CollectRep.MetricsData.Builder builder, POP3Client pop3Client, + List<String> aliasFields, long responseTime) throws IOException { + Map<String, Object> pop3Metrics = parsePop3Metrics(pop3Client, aliasFields); + + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + Object value = pop3Metrics.get(alias); + if (value != null) { + valueRowBuilder.addColumn(String.valueOf(value)); + } else { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(String.valueOf(responseTime)); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + } + builder.addValueRow(valueRowBuilder.build()); + } + + private Map<String, Object> parsePop3Metrics(POP3Client pop3Client, List<String> aliasFields) throws IOException { + Map<String, Object> pop3Metrics = new HashMap<>(aliasFields.size()); + POP3MessageInfo status = pop3Client.status(); + int emailCount = 0; + double mailboxSize = 0.0; + if (status != null) { + emailCount = status.number; + // bytes to KB + mailboxSize = (double) status.size / 1024.0; + pop3Metrics.put(EMAIL_COUNT, emailCount); + pop3Metrics.put(MAILBOX_SIZE, mailboxSize); + } + return pop3Metrics; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/PrometheusAutoCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/PrometheusAutoCollectImpl.java new file mode 100644 index 0000000..8fa1996 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/PrometheusAutoCollectImpl.java @@ -0,0 +1,329 @@ +/* + * 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.collector.collect.prometheus; + +import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.net.ConnectException; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.SSLException; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.collector.collect.prometheus.parser.MetricFamily; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.PrometheusProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.Base64Util; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.apache.hertzbeat.collector.collect.prometheus.parser.OnlineParser; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpHost; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.DigestScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.protocol.HttpContext; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.util.StringUtils; + +/** + * prometheus auto collect + */ +@Slf4j +public class PrometheusAutoCollectImpl implements PrometheusCollect { + + private final Set<Integer> defaultSuccessStatusCodes = Stream.of(HttpStatus.SC_OK, HttpStatus.SC_CREATED, + HttpStatus.SC_ACCEPTED, HttpStatus.SC_MULTIPLE_CHOICES, HttpStatus.SC_MOVED_PERMANENTLY, + HttpStatus.SC_MOVED_TEMPORARILY).collect(Collectors.toSet()); + + @Override + public List<CollectRep.MetricsData> collect(CollectRep.MetricsData.Builder builder, + Metrics metrics) { + try { + validateParams(metrics); + } catch (Exception e) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(e.getMessage()); + return null; + } + HttpContext httpContext = createHttpContext(metrics.getPrometheus()); + HttpUriRequest request = createHttpRequest(metrics.getPrometheus()); + try (CloseableHttpResponse response = + CommonHttpClient.getHttpClient().execute(request, httpContext)) { + int statusCode = response.getStatusLine().getStatusCode(); + boolean isSuccessInvoke = defaultSuccessStatusCodes.contains(statusCode); + log.debug("http response status: {}", statusCode); + if (!isSuccessInvoke) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode); + return null; + } + try { + return parseResponseByPrometheusExporter(response.getEntity().getContent(), builder); + } catch (Exception e) { + log.info("parse error: {}.", e.getMessage(), e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("parse response data error:" + e.getMessage()); + } + } catch (ClientProtocolException e1) { + String errorMsg = CommonUtil.getMessageFromThrowable(e1); + log.error(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (UnknownHostException e2) { + String errorMsg = CommonUtil.getMessageFromThrowable(e2); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_REACHABLE); + builder.setMsg("unknown host:" + errorMsg); + } catch (InterruptedIOException | ConnectException | SSLException e3) { + String errorMsg = CommonUtil.getMessageFromThrowable(e3); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (IOException e4) { + String errorMsg = CommonUtil.getMessageFromThrowable(e4); + log.info(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error(errorMsg, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (request != null) { + request.abort(); + } + } + return Collections.singletonList(builder.build()); + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_PROMETHEUS; + } + + private void validateParams(Metrics metrics) throws Exception { + if (metrics == null || metrics.getPrometheus() == null) { + throw new Exception("Prometheus collect must has prometheus params"); + } + PrometheusProtocol protocol = metrics.getPrometheus(); + if (protocol.getPath() == null + || !StringUtils.hasText(protocol.getPath()) + || !protocol.getPath().startsWith(RIGHT_DASH)) { + protocol.setPath(protocol.getPath() == null ? RIGHT_DASH : RIGHT_DASH + protocol.getPath().trim()); + } + } + + private List<CollectRep.MetricsData> parseResponseByPrometheusExporter(InputStream inputStream, CollectRep.MetricsData.Builder builder) throws IOException { + long endTime = System.currentTimeMillis(); + builder.setTime(endTime); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + List<CollectRep.MetricsData> metricsDataList = new LinkedList<>(); + if (metricFamilyMap == null) { + return metricsDataList; + } + for (Map.Entry<String, MetricFamily> entry : metricFamilyMap.entrySet()) { + builder.clearFields(); + builder.clearValues(); + String metricsName = entry.getKey(); + builder.setMetrics(metricsName); + MetricFamily metricFamily = entry.getValue(); + if (!metricFamily.getMetricList().isEmpty()) { + List<String> metricsFields = new LinkedList<>(); + for (int index = 0; index < metricFamily.getMetricList().size(); index++) { + MetricFamily.Metric metric = metricFamily.getMetricList().get(index); + if (index == 0) { + metric.getLabels().forEach(label -> { + metricsFields.add(label.getName()); + builder.addField(CollectRep.Field.newBuilder().setName(label.getName()) + .setType(CommonConstants.TYPE_STRING).setLabel(true).build()); + }); + builder.addField(CollectRep.Field.newBuilder().setName("metric_value") + .setType(CommonConstants.TYPE_NUMBER).setLabel(false).build()); + } + Map<String, String> labelMap = metric.getLabels() + .stream() + .collect(Collectors.toMap(MetricFamily.Label::getName, MetricFamily.Label::getValue)); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String field : metricsFields) { + String fieldValue = labelMap.get(field); + valueRowBuilder.addColumn(fieldValue == null ? CommonConstants.NULL_VALUE : fieldValue); + } + valueRowBuilder.addColumn(String.valueOf(metric.getValue())); + builder.addValueRow(valueRowBuilder.build()); + } + metricsDataList.add(builder.build()); + } + } + return metricsDataList; + } + + /** + * create httpContext + * + * @param protocol prometheus protocol + * @return context + */ + public HttpContext createHttpContext(PrometheusProtocol protocol) { + PrometheusProtocol.Authorization auth = protocol.getAuthorization(); + if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) { + HttpClientContext clientContext = new HttpClientContext(); + if (StringUtils.hasText(auth.getDigestAuthUsername()) + && StringUtils.hasText(auth.getDigestAuthPassword())) { + CredentialsProvider provider = new BasicCredentialsProvider(); + UsernamePasswordCredentials credentials = + new UsernamePasswordCredentials(auth.getDigestAuthUsername(), auth.getDigestAuthPassword()); + provider.setCredentials(AuthScope.ANY, credentials); + AuthCache authCache = new BasicAuthCache(); + authCache.put(new HttpHost(protocol.getHost(), Integer.parseInt(protocol.getPort())), new DigestScheme()); + clientContext.setCredentialsProvider(provider); + clientContext.setAuthCache(authCache); + return clientContext; + } + } + return null; + } + + /** + * create http request + * @param protocol http params + * @return http uri request + */ + public HttpUriRequest createHttpRequest(PrometheusProtocol protocol) { + RequestBuilder requestBuilder = RequestBuilder.get(); + // params + Map<String, String> params = protocol.getParams(); + if (params != null && !params.isEmpty()) { + for (Map.Entry<String, String> param : params.entrySet()) { + if (StringUtils.hasText(param.getValue())) { + requestBuilder.addParameter(param.getKey(), param.getValue()); + } + } + } + // The default request header can be overridden if customized + // keep-alive + requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE); + requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT); + // headers The custom request header is overwritten here + Map<String, String> headers = protocol.getHeaders(); + if (headers != null && !headers.isEmpty()) { + for (Map.Entry<String, String> header : headers.entrySet()) { + if (StringUtils.hasText(header.getValue())) { + requestBuilder.addHeader(CollectUtil.replaceUriSpecialChar(header.getKey()), + CollectUtil.replaceUriSpecialChar(header.getValue())); + } + } + } + // add accept + requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE); + + if (protocol.getAuthorization() != null) { + PrometheusProtocol.Authorization authorization = protocol.getAuthorization(); + if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) { + String value = DispatchConstants.BEARER + " " + authorization.getBearerTokenToken(); + requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value); + } else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) { + if (StringUtils.hasText(authorization.getBasicAuthUsername()) + && StringUtils.hasText(authorization.getBasicAuthPassword())) { + String authStr = authorization.getBasicAuthUsername() + ":" + authorization.getBasicAuthPassword(); + String encodedAuth = Base64Util.encode(authStr); + requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + " " + encodedAuth); + } + } + } + + // if it has payload, would override post params + if (StringUtils.hasLength(protocol.getPayload()) && (HttpMethod.POST.matches(protocol.getMethod()) || HttpMethod.PUT.matches(protocol.getMethod()))) { + requestBuilder.setEntity(new StringEntity(protocol.getPayload(), StandardCharsets.UTF_8)); + } + + // uri + String uri = CollectUtil.replaceUriSpecialChar(protocol.getPath()); + if (IpDomainUtil.isHasSchema(protocol.getHost())) { + + requestBuilder.setUri(protocol.getHost() + SignConstants.DOUBLE_MARK + protocol.getPort() + uri); + } else { + String ipAddressType = IpDomainUtil.checkIpAddressType(protocol.getHost()); + String baseUri = NetworkConstants.IPV6.equals(ipAddressType) + ? String.format("[%s]:%s%s", protocol.getHost(), protocol.getPort(), uri) + : String.format("%s:%s%s", protocol.getHost(), protocol.getPort(), uri); + boolean ssl = Boolean.parseBoolean(protocol.getSsl()); + if (ssl) { + requestBuilder.setUri(NetworkConstants.HTTPS_HEADER + baseUri); + } else { + requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri); + } + } + + // custom timeout + int timeout = CollectUtil.getTimeout(protocol.getTimeout(), 0); + if (timeout > 0) { + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(timeout) + .setSocketTimeout(timeout) + .setRedirectsEnabled(true) + .build(); + requestBuilder.setConfig(requestConfig); + } + return requestBuilder.build(); + } + + /** + * get collect instance + * @return instance + */ + public static PrometheusAutoCollectImpl getInstance() { + return PrometheusAutoCollectImpl.SingleInstance.INSTANCE; + } + + /** + * static instance + */ + private static class SingleInstance { + private static final PrometheusAutoCollectImpl INSTANCE = new PrometheusAutoCollectImpl(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/PrometheusCollect.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/PrometheusCollect.java new file mode 100644 index 0000000..7935b4b --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/PrometheusCollect.java @@ -0,0 +1,45 @@ +/* + * 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.collector.collect.prometheus; + +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +import java.util.List; + +/** + * Prometheus metrics collector interface + */ +public interface PrometheusCollect { + + /** + * Collect prometheus metrics data + * @param builder metrics data builder + * @param metrics metrics config + * @return list of metrics data + */ + List<CollectRep.MetricsData> collect(CollectRep.MetricsData.Builder builder, Metrics metrics); + + /** + * Get the protocol name this collector supported + * @return protocol name + */ + String supportProtocol(); +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/PrometheusProxyCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/PrometheusProxyCollectImpl.java new file mode 100644 index 0000000..2177710 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/PrometheusProxyCollectImpl.java @@ -0,0 +1,308 @@ +/* + * 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.collector.collect.prometheus; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.PrometheusProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.Base64Util; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpHost; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.DigestScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.springframework.http.MediaType; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.ConnectException; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.SSLException; + +import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH; + + +@Slf4j +public class PrometheusProxyCollectImpl implements PrometheusCollect { + + private final Set<Integer> defaultSuccessStatusCodes = Stream.of(HttpStatus.SC_OK, HttpStatus.SC_CREATED, + HttpStatus.SC_ACCEPTED, HttpStatus.SC_MULTIPLE_CHOICES, HttpStatus.SC_MOVED_PERMANENTLY, + HttpStatus.SC_MOVED_TEMPORARILY).collect(Collectors.toSet()); + + public static final String RAW_TEXT_CONTENT_FIELD_NAME = "raw_text_content"; + + @Override + public List<CollectRep.MetricsData> collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + PrometheusProtocol prometheusProtocol = metrics.getPrometheus(); + HttpUriRequest request; + try { + validateParams(metrics); + } catch (Exception e) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(e.getMessage()); + return Collections.singletonList(builder.build()); + } + + HttpContext httpContext = createHttpContext(prometheusProtocol); + request = createHttpRequest(prometheusProtocol); + + try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) { + int statusCode = response.getStatusLine().getStatusCode(); + log.debug("Prometheus proxy collect, response status: {}", statusCode); + + if (!defaultSuccessStatusCodes.contains(statusCode)) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode); + return Collections.singletonList(builder.build()); + } + + String rawTextContent = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + + builder.clearFields(); + builder.clearValues(); + + CollectRep.Field rawDataField = CollectRep.Field.newBuilder() + .setName(RAW_TEXT_CONTENT_FIELD_NAME) + .setType(CommonConstants.TYPE_STRING) + .build(); + builder.addField(rawDataField); + + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + valueRowBuilder.addColumn(rawTextContent); + builder.addValueRow(valueRowBuilder.build()); + + builder.setCode(CollectRep.Code.SUCCESS); + } catch (ClientProtocolException e1) { + String errorMsg = CommonUtil.getMessageFromThrowable(e1); + log.error("Prometheus proxy collect error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e1); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (UnknownHostException e2) { + String errorMsg = CommonUtil.getMessageFromThrowable(e2); + log.info("Prometheus proxy collect unknown host: {}. Host: {}", errorMsg, prometheusProtocol.getHost(), e2); + builder.setCode(CollectRep.Code.UN_REACHABLE); + builder.setMsg("unknown host:" + errorMsg); + } catch (InterruptedIOException | ConnectException | SSLException e3) { + String errorMsg = CommonUtil.getMessageFromThrowable(e3); + log.info("Prometheus proxy collect connect error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e3); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (IOException e4) { + String errorMsg = CommonUtil.getMessageFromThrowable(e4); + log.info("Prometheus proxy collect IO error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e4); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error("Prometheus proxy collect unknown error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (request != null) { + request.abort(); + } + } + return Collections.singletonList(builder.build()); + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_PROMETHEUS; + } + + private void validateParams(Metrics metrics) throws Exception { + if (metrics == null || metrics.getPrometheus() == null) { + throw new Exception("Prometheus collect must has prometheus params"); + } + PrometheusProtocol protocol = metrics.getPrometheus(); + if (!StringUtils.hasText(protocol.getHost()) + || !StringUtils.hasText(protocol.getPort())) { + throw new Exception("Prometheus collect must has host and port params"); + } + if (protocol.getPath() == null + || !StringUtils.hasText(protocol.getPath()) + || !protocol.getPath().startsWith(RIGHT_DASH)) { + protocol.setPath(protocol.getPath() == null ? RIGHT_DASH : RIGHT_DASH + protocol.getPath().trim()); + } + } + + /** + * create httpContext + * This method is adapted from PrometheusAutoCollectImpl + * @param protocol prometheus protocol + * @return context + */ + public HttpContext createHttpContext(PrometheusProtocol protocol) { + PrometheusProtocol.Authorization auth = protocol.getAuthorization(); + if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) { + HttpClientContext clientContext = new HttpClientContext(); + if (StringUtils.hasText(auth.getDigestAuthUsername()) + && StringUtils.hasText(auth.getDigestAuthPassword())) { + CredentialsProvider provider = new BasicCredentialsProvider(); + UsernamePasswordCredentials credentials = + new UsernamePasswordCredentials(auth.getDigestAuthUsername(), auth.getDigestAuthPassword()); + provider.setCredentials(AuthScope.ANY, credentials); + AuthCache authCache = new BasicAuthCache(); + HttpHost targetHost = new HttpHost(protocol.getHost(), Integer.parseInt(protocol.getPort())); + authCache.put(targetHost, new DigestScheme()); + clientContext.setCredentialsProvider(provider); + clientContext.setAuthCache(authCache); + return clientContext; + } + } + return null; + } + + /** + * create http request + * This method is adapted from PrometheusAutoCollectImpl + * @param protocol http params + * @return http uri request + */ + public HttpUriRequest createHttpRequest(PrometheusProtocol protocol) { + RequestBuilder requestBuilder = RequestBuilder.get(); + // params + Map<String, String> params = protocol.getParams(); + if (params != null && !params.isEmpty()) { + for (Map.Entry<String, String> param : params.entrySet()) { + if (StringUtils.hasText(param.getValue())) { + requestBuilder.addParameter(param.getKey(), param.getValue()); + } + } + } + requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE); + requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT); + // headers The custom request header is overwritten here + Map<String, String> headers = protocol.getHeaders(); + if (headers != null && !headers.isEmpty()) { + for (Map.Entry<String, String> header : headers.entrySet()) { + if (StringUtils.hasText(header.getValue())) { + requestBuilder.addHeader(CollectUtil.replaceUriSpecialChar(header.getKey()), + CollectUtil.replaceUriSpecialChar(header.getValue())); + } + } + } + if (headers == null || headers.keySet().stream().noneMatch(HttpHeaders.ACCEPT::equalsIgnoreCase)) { + requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE + ";version=0.0.4,*/*;q=0.1"); + } + + if (protocol.getAuthorization() != null) { + PrometheusProtocol.Authorization authorization = protocol.getAuthorization(); + if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) { + if (StringUtils.hasText(authorization.getBearerTokenToken())) { + String value = DispatchConstants.BEARER + " " + authorization.getBearerTokenToken(); + requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value); + } + } else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) { + if (StringUtils.hasText(authorization.getBasicAuthUsername()) + && StringUtils.hasText(authorization.getBasicAuthPassword())) { + String authStr = authorization.getBasicAuthUsername() + ":" + authorization.getBasicAuthPassword(); + String encodedAuth = Base64Util.encode(authStr); + requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + " " + encodedAuth); + } + } + } + + if (StringUtils.hasLength(protocol.getPayload())) { + requestBuilder.setEntity(new StringEntity(protocol.getPayload(), StandardCharsets.UTF_8)); + if (headers == null || headers.keySet().stream().noneMatch(HttpHeaders.CONTENT_TYPE::equalsIgnoreCase)) { + requestBuilder.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE); + } + } + + String uriPath = CollectUtil.replaceUriSpecialChar(protocol.getPath()); + if (IpDomainUtil.isHasSchema(protocol.getHost())) { + requestBuilder.setUri(protocol.getHost() + SignConstants.DOUBLE_MARK + protocol.getPort() + uriPath); + } else { + String ipAddressType = IpDomainUtil.checkIpAddressType(protocol.getHost()); + String baseUri = NetworkConstants.IPV6.equals(ipAddressType) + ? String.format("[%s]:%s%s", protocol.getHost(), protocol.getPort(), uriPath) + : String.format("%s:%s%s", protocol.getHost(), protocol.getPort(), uriPath); + boolean ssl = Boolean.parseBoolean(protocol.getSsl()); + if (ssl) { + requestBuilder.setUri(NetworkConstants.HTTPS_HEADER + baseUri); + } else { + requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri); + } + } + + // custom timeout + int timeout = CollectUtil.getTimeout(protocol.getTimeout()); + if (timeout > 0) { + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(timeout) + .setSocketTimeout(timeout) + .setConnectionRequestTimeout(timeout) + .setRedirectsEnabled(true) + .build(); + requestBuilder.setConfig(requestConfig); + } else { + RequestConfig requestConfig = RequestConfig.custom() + .setRedirectsEnabled(true) + .build(); + requestBuilder.setConfig(requestConfig); + } + return requestBuilder.build(); + } + + /** + * get collect instance + * @return instance + */ + public static PrometheusProxyCollectImpl getInstance() { + return PrometheusProxyCollectImpl.SingleInstance.INSTANCE; + } + + /** + * static instance + */ + private static class SingleInstance { + private static final PrometheusProxyCollectImpl INSTANCE = new PrometheusProxyCollectImpl(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/MetricFamily.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/MetricFamily.java new file mode 100644 index 0000000..1d9e5a3 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/MetricFamily.java @@ -0,0 +1,74 @@ +/* + * 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.collector.collect.prometheus.parser; + +import java.util.List; +import lombok.Data; +import lombok.ToString; + +/** + * metric family + */ +@Data +@ToString +public class MetricFamily { + + /** + * metric name + */ + private String name; + + /** + * metrics + */ + private List<Metric> metricList; + + /** + * Metric + */ + @Data + public static class Metric { + + /** + * labels + */ + private List<Label> labels; + + /** + * value + */ + private double value; + } + + /** + * Label + */ + @Data + public static class Label { + + /** + * name + */ + private String name; + + /** + * value + */ + private String value; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParser.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParser.java new file mode 100644 index 0000000..de48f59 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParser.java @@ -0,0 +1,619 @@ +/* + * 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.collector.collect.prometheus.parser; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * Resolves the data passed by prometheus's exporter interface http:xxx/metrics + */ +@Slf4j +public class OnlineParser { + + private static final Map<Integer, Integer> escapeMap = new HashMap<>(8); + + private static final char RIGHT_BRACKET = '}'; + private static final char LEFT_BRACKET = '{'; + private static final char UTF8_REPLACEMENT_CHARACTER = '\uFFFD'; + + static { + escapeMap.put((int) 'n', (int) '\n'); + escapeMap.put((int) 'b', (int) '\b'); + escapeMap.put((int) 't', (int) '\t'); + escapeMap.put((int) 'r', (int) '\r'); + escapeMap.put((int) 'f', (int) '\f'); + escapeMap.put((int) '\'', (int) '\''); + escapeMap.put((int) '\"', (int) '\"'); + escapeMap.put((int) '\\', (int) '\\'); + } + + private OnlineParser() { + } + + public static Map<String, MetricFamily> parseMetrics(InputStream inputStream) throws IOException { + Map<String, MetricFamily> metricFamilyMap = new ConcurrentHashMap<>(10); + try { + int i = getChar(inputStream); + while (i != -1) { + if (i == '#' || i == '\n') { + skipToLineEnd(inputStream).maybeEol().maybeEof().noElse(); + } else { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append((char) i); + parseMetric(inputStream, metricFamilyMap, stringBuilder); + } + i = getChar(inputStream); + // To address the `\n\r` scenario, it is necessary to skip + if (i == '\r') { + i = getChar(inputStream); + } + } + } catch (FormatException e) { + log.error("prometheus parser failed because of wrong input format. {}", e.getMessage()); + return null; + } + return metricFamilyMap; + } + + /** + * Parses Prometheus metrics from the given {@link InputStream}, but only for the specified metric name. + * <p> + * This method differs from {@link #parseMetrics(InputStream)} in that it filters and parses only the metric + * with the given name, rather than parsing all available metrics from the input stream. + * + * @param inputStream the input stream containing Prometheus metrics data + * @param metric the name of the metric to filter and parse (case-insensitive) + * @return a map of metric family names to {@link MetricFamily} objects, or {@code null} if parsing fails + * @throws IOException if an I/O error occurs while reading from the input stream + */ + public static Map<String, MetricFamily> parseMetrics(InputStream inputStream, String metric) throws IOException { + Map<String, MetricFamily> metricFamilyMap = new ConcurrentHashMap<>(10); + try { + int i = getChar(inputStream); + while (i != -1) { + if (i == '#' || i == '\n') { + skipToLineEnd(inputStream).maybeEol().maybeEof().noElse(); + } else { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append((char) i); + + // parse the metricName to filter. + int next = parseMetricName(inputStream, stringBuilder).maybeSpace().maybeLeftBracket().noElse(); + String metricName = stringBuilder.toString(); + stringBuilder.delete(0, stringBuilder.length()); + + // step2: Determine whether this metric should be parsed. + if (metric.equals(metricName)) { + parseMetricFromName(inputStream, stringBuilder, metricFamilyMap, metricName, next); + } else { + skipToLineEnd(inputStream).maybeEol().maybeEof().noElse(); + } + } + i = getChar(inputStream); + // To address the `\n\r` scenario, it is necessary to skip + if (i == '\r') { + i = getChar(inputStream); + } + } + } catch (FormatException e) { + log.error("prometheus parser failed because of wrong input format. {}", e.getMessage()); + return null; + } + return metricFamilyMap; + } + + /** + * Start parsing the complete metric from the already parsed metric name. + * + * @param inputStream the input stream containing the metric data + * @param stringBuilder the StringBuilder used for parsing and temporary storage + * @param metricFamilyMap the map to store parsed MetricFamily objects, keyed by metric name + * @param metricName the name of the metric to parse + * @param next the next character to process from the input stream + * @return a CharChecker containing the next character after parsing the metric + * @throws IOException if an I/O error occurs while reading the input stream + * @throws FormatException if the input format is invalid + */ + private static CharChecker parseMetricFromName(InputStream inputStream, StringBuilder stringBuilder, + Map<String, MetricFamily> metricFamilyMap, + String metricName, int next) throws IOException, FormatException { + MetricFamily metricFamily; + MetricFamily.Metric metric = new MetricFamily.Metric(); + + if (!metricFamilyMap.containsKey(metricName)) { + metricFamily = new MetricFamily(); + metricFamily.setMetricList(new ArrayList<>()); + metricFamily.setName(metricName); + metricFamilyMap.put(metricName, metricFamily); + } else { + metricFamily = metricFamilyMap.get(metricName); + } + int i = next; + if (i == ' ') { + i = skipSpaces(inputStream).getInt(); + } + + List<MetricFamily.Label> labelList = new LinkedList<>(); + metric.setLabels(labelList); + if (i == '{') { + parseLabels(inputStream, stringBuilder, labelList); + i = skipSpaces(inputStream).getInt(); + } + + stringBuilder.delete(0, stringBuilder.length()); + stringBuilder.append((char) i); + i = parseOneDouble(inputStream, stringBuilder).maybeSpace().maybeEol().maybeEof().noElse(); + metric.setValue(toDouble(stringBuilder.toString())); + if (i == '\n' || i == -1) { + metricFamily.getMetricList().add(metric); + return new CharChecker(i); + } + + i = skipSpaces(inputStream).getInt(); + stringBuilder.delete(0, stringBuilder.length()); + stringBuilder.append((char) i); + i = skipOneLong(inputStream).maybeSpace().maybeEol().maybeEof().noElse(); + if (i == '\n' || i == -1) { + metricFamily.getMetricList().add(metric); + return new CharChecker(i); + } + i = skipSpaces(inputStream).maybeEol().maybeEof().noElse(); + + metricFamily.getMetricList().add(metric); + return new CharChecker(i); + } + + private static class FormatException extends Exception { + + public FormatException() { + } + + public FormatException(String message) { + super(message); + } + } + + private static class CharChecker { + int i; + boolean satisfied; + + CharChecker(int i) { + this.i = i; + this.satisfied = false; + } + + private CharChecker maybeLeftBracket() { + if (i == LEFT_BRACKET) { + satisfied = true; + } + return this; + } + + private CharChecker maybeRightBracket() { + if (i == RIGHT_BRACKET) { + satisfied = true; + } + return this; + } + + private CharChecker maybeEqualsSign() { + if (i == '=') { + satisfied = true; + } + return this; + } + + private CharChecker maybeQuotationMark() { + if (i == '"') { + satisfied = true; + } + return this; + } + + private CharChecker maybeSpace() { + if (i == ' ') { + satisfied = true; + } + return this; + } + + private CharChecker maybeComma() { + if (i == ',') { + satisfied = true; + } + return this; + } + + private CharChecker maybeEof() { + if (i == -1) { + satisfied = true; + } + return this; + } + + private CharChecker maybeEol() { + if (i == '\n') { + satisfied = true; + } + return this; + } + + private int noElse() throws FormatException { + if (!satisfied) { + throw new FormatException(); + } + return this.i; + } + + private int getInt() { + return this.i; + } + + } + + private static int getChar(InputStream inputStream) throws IOException, FormatException { + int i = inputStream.read(); + if (i == '\\') { + i = inputStream.read(); + if (escapeMap.containsKey(i)) { + return escapeMap.get(i); + } else { + throw new FormatException("Escape character failed."); + } + } else { + return i; + } + } + + private static CharChecker parseOneDouble(InputStream inputStream, StringBuilder stringBuilder) throws IOException, FormatException { + int i = getChar(inputStream); + while (i >= '0' && i <= '9' || i >= 'a' && i <= 'z' || i >= 'A' && i <= 'Z' || i == '-' || i == '+' || i == '.') { + stringBuilder.append((char) i); + i = getChar(inputStream); + } + // Skip \r character to handle Windows line endings + if (i == '\r') { + i = getChar(inputStream); + } + return new CharChecker(i); + } + + private static CharChecker skipOneLong(InputStream inputStream) throws IOException, FormatException { + int i = getChar(inputStream); + while (i >= '0' && i <= '9') { + i = getChar(inputStream); + } + // Skip \r character to handle Windows line endings + if (i == '\r') { + i = getChar(inputStream); + } + return new CharChecker(i); + } + + private static CharChecker parseMetricName(InputStream inputStream, StringBuilder stringBuilder) throws IOException, FormatException { + int i = getChar(inputStream); + while ((i >= 'a' && i <= 'z') || (i >= 'A' && i <= 'Z') || (i >= '0' && i <= '9') || i == '_' || i == ':') { + stringBuilder.append((char) i); + i = getChar(inputStream); + } + return new CharChecker(i); + } + + private static CharChecker parseLabelName(InputStream inputStream, StringBuilder stringBuilder) throws IOException, FormatException { + int i = getChar(inputStream); + while ((i >= 'a' && i <= 'z') || (i >= 'A' && i <= 'Z') || (i >= '0' && i <= '9') || i == '_') { + stringBuilder.append((char) i); + i = getChar(inputStream); + } + return new CharChecker(i); + } + + private static CharChecker parseLabelValue(InputStream inputStream, StringBuilder stringBuilder) throws IOException, FormatException { + int i = inputStream.read(); + while (i != '"' && i != -1) { + if (i == '\\') { + i = inputStream.read(); + switch (i) { + case 'n' -> stringBuilder.append('\n'); + case '\\' -> stringBuilder.append('\\'); + case '\"' -> stringBuilder.append('\"'); + default -> { + // Unknown escape, keep as-is + // https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/lib/protoparser/prometheus/parser.go#L419 + stringBuilder.append('\\'); + if (i != -1) { + stringBuilder.append((char) i); + } + } + } + } else if (i <= 127) { + stringBuilder.append((char) i); + } else { + handleUtf8Character(i, inputStream, stringBuilder); + } + i = inputStream.read(); + } + return new CharChecker(i); + } + + /** + * Handles multi-byte UTF-8 character parsing from input stream. + * Reads additional bytes based on the first byte and validates the UTF-8 sequence. + * Appends the decoded character to the string builder or replacement character if invalid. + * + * @param firstByte the first byte of the UTF-8 character sequence + * @param inputStream the input stream to read additional bytes from + * @param stringBuilder the string builder to append the decoded character to + * @throws IOException if an I/O error occurs while reading from the input stream + */ + private static void handleUtf8Character(int firstByte, InputStream inputStream, StringBuilder stringBuilder) throws IOException { + byte[] byteArray = new byte[4]; + byteArray[0] = (byte) firstByte; + int additionalBytes = calculateUtf8ContinuationBytes(firstByte); + if (additionalBytes == -1) { + appendInvalidCharacters(stringBuilder); + return; + } + int totalBytes = 1; + + for (int i = 0; i < additionalBytes; i++) { + int nextByte = inputStream.read(); + if (nextByte == -1) { + appendInvalidCharacters(stringBuilder); + return; + } + // Verify subsequent byte format:10xxxxxx + if ((nextByte & 0xC0) != 0x80) { + appendInvalidCharacters(stringBuilder); + return; + } + byteArray[i + 1] = (byte) nextByte; + totalBytes++; + } + try { + // todo: If stricter UTF-8 semantic validation is required,boundary conditions are slightly strengthened. + String utf8Chars = new String(byteArray, 0, totalBytes, StandardCharsets.UTF_8); + stringBuilder.append(utf8Chars); + } catch (Exception e) { + log.debug("Invalid UTF-8 sequence detected at firstByte: {}", Integer.toHexString(firstByte)); + appendInvalidCharacters(stringBuilder); + } + } + + /** + * Appends the UTF-8 replacement character (\uFFFD) to the StringBuilder. + * This method is used to append the replacement character to the string builder + * when invalid UTF-8 byte sequences are encountered during parsing. + * The replacement character (U+FFFD) is a special Unicode character used to + * represent characters that cannot be decoded properly. + * + * @param stringBuilder the string builder to append to, no operation if null + */ + private static void appendInvalidCharacters(StringBuilder stringBuilder) { + Optional.ofNullable(stringBuilder).ifPresent(t -> t.append(UTF8_REPLACEMENT_CHARACTER)); + } + + /** + * Checks if a label value contains invalid characters. + * This method is used to validate the validity of Prometheus label values. + * If the label value contains the UTF-8 replacement character (\uFFFD), + * it is considered invalid because the replacement character indicates that + * byte sequences that could not be properly decoded were encountered during parsing. + * + * According to Prometheus specifications, label values should not contain + * replacement characters as this would cause data inconsistency and query issues. + * + * @param labelValue the label value to check + * @return true if the label value is not blank and contains UTF-8 replacement character, false otherwise + */ + private static boolean isInvalidLabelValue(String labelValue) { + return StringUtils.isNotBlank(labelValue) && labelValue.contains(String.valueOf(UTF8_REPLACEMENT_CHARACTER)); + } + + /** + * Calculates the number of continuation bytes needed for a UTF-8 character. + * This method analyzes the first byte of a UTF-8 encoding to determine how many + * continuation bytes (10xxxxxx pattern) are required to complete the character. + * + * UTF-8 encoding rules: + * - 1-byte character: 0xxxxxxx (ASCII characters, this method won't be called) + * - 2-byte character: 110xxxxx 10xxxxxx (returns 1 continuation byte) + * - 3-byte character: 1110xxxx 10xxxxxx 10xxxxxx (returns 2 continuation bytes) + * - 4-byte character: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (returns 3 continuation bytes) + * + * Also validates byte sequences: + * - 0xC0 and 0xC1: overlong encoding, invalid + * - 0xF5-0xFF: out of Unicode range, invalid + * + * Note: A basic Overlong Encoding check has been added here. + * If stricter and more comprehensive validation is required later, + * this method should be separated out to serve solely as a check for the first character. + * + * @param firstByte the first byte of the UTF-8 character sequence + * @return the number of continuation bytes needed, or -1 if the first byte is invalid + * - returns 1: 2-byte character (needs 1 continuation byte) + * - returns 2: 3-byte character (needs 2 continuation bytes) + * - returns 3: 4-byte character (needs 3 continuation bytes) + * - returns -1: invalid first byte + */ + private static int calculateUtf8ContinuationBytes(int firstByte) { + if ((firstByte & 0xE0) == 0xC0) { + if (firstByte <= 0xC1) { + return -1; + } + return 1; + } + if ((firstByte & 0xF0) == 0xE0) { + return 2; + } + if ((firstByte & 0xF8) == 0xF0) { + if (firstByte >= 0xF5) { + return -1; + } + return 3; + } + return -1; + } + + private static CharChecker skipSpaces(InputStream inputStream) throws IOException, FormatException { + int i = getChar(inputStream); + while (i == ' ') { + i = getChar(inputStream); + } + return new CharChecker(i); + } + + private static CharChecker skipToLineEnd(InputStream inputStream) throws IOException, FormatException { + int i = getChar(inputStream); + while (i != '\n' && i != -1) { + i = getChar(inputStream); + } + return new CharChecker(i); + } + + private static Double toDouble(String string) throws FormatException { + switch (string) { + case "+Inf": + return Double.POSITIVE_INFINITY; + case "-Inf": + return Double.NEGATIVE_INFINITY; + case "NaN": + return Double.NaN; + default: + try { + BigDecimal bigDecimal = new BigDecimal(string); + return bigDecimal.doubleValue(); + } catch (NumberFormatException e) { + throw new FormatException(); + } + } + } + + + /** + * parse single label like label_name="label_value" + */ + private static CharChecker parseLabel(InputStream inputStream, StringBuilder stringBuilder, List<MetricFamily.Label> labelList) throws IOException, FormatException { + int i; + MetricFamily.Label label = new MetricFamily.Label(); + i = skipSpaces(inputStream).getInt(); + if (i == RIGHT_BRACKET) { + return new CharChecker(i); + } + stringBuilder.append((char) i); + i = parseLabelName(inputStream, stringBuilder).maybeSpace().maybeEqualsSign().noElse(); + label.setName(stringBuilder.toString()); + stringBuilder.delete(0, stringBuilder.length()); + if (i == ' ') { + skipSpaces(inputStream).maybeEqualsSign().noElse(); + } + + skipSpaces(inputStream).maybeQuotationMark().noElse(); + parseLabelValue(inputStream, stringBuilder).maybeQuotationMark().noElse(); + String labelValue = stringBuilder.toString(); + if (isInvalidLabelValue(labelValue)) { + log.error("Invalid UTF-8 sequence detected at labelValue."); + throw new FormatException(); + } + label.setValue(labelValue); + stringBuilder.delete(0, stringBuilder.length()); + labelList.add(label); + return new CharChecker(i); + } + + + private static void parseLabels(InputStream inputStream, StringBuilder stringBuilder, List<MetricFamily.Label> labelList) throws IOException, FormatException { + int i; + while (true) { + // deal with labels like {label1="aaa",label2=bbb",} + CharChecker charChecker = parseLabel(inputStream, stringBuilder, labelList); + if (charChecker.i == RIGHT_BRACKET) { + return; + } + //deal with labels like {label1="aaa",label2=bbb"} + i = skipSpaces(inputStream).maybeSpace().maybeComma().maybeRightBracket().noElse(); + if (i == RIGHT_BRACKET) { + return; + } + } + } + + private static CharChecker parseMetric(InputStream inputStream, Map<String, MetricFamily> metricFamilyMap, StringBuilder stringBuilder) throws IOException, FormatException { + MetricFamily metricFamily; + MetricFamily.Metric metric = new MetricFamily.Metric(); + int i = parseMetricName(inputStream, stringBuilder).maybeSpace().maybeLeftBracket().noElse(); + String metricName = stringBuilder.toString(); + stringBuilder.delete(0, stringBuilder.length()); + + if (!metricFamilyMap.containsKey(metricName)) { + metricFamily = new MetricFamily(); + metricFamily.setMetricList(new ArrayList<>()); + metricFamily.setName(metricName); + metricFamilyMap.put(metricName, metricFamily); + } else { + metricFamily = metricFamilyMap.get(metricName); + } + + if (i == ' ') { + i = skipSpaces(inputStream).getInt(); + } + + List<MetricFamily.Label> labelList = new LinkedList<>(); + metric.setLabels(labelList); + if (i == '{') { + parseLabels(inputStream, stringBuilder, labelList); + i = skipSpaces(inputStream).getInt(); + } + + stringBuilder.delete(0, stringBuilder.length()); + stringBuilder.append((char) i); + i = parseOneDouble(inputStream, stringBuilder).maybeSpace().maybeEol().maybeEof().noElse(); + metric.setValue(toDouble(stringBuilder.toString())); + if (i == '\n' || i == -1) { + metricFamily.getMetricList().add(metric); + return new CharChecker(i); + } + + i = skipSpaces(inputStream).getInt(); + stringBuilder.delete(0, stringBuilder.length()); + stringBuilder.append((char) i); + i = skipOneLong(inputStream).maybeSpace().maybeEol().maybeEof().noElse(); + if (i == '\n' || i == -1) { + metricFamily.getMetricList().add(metric); + return new CharChecker(i); + } + i = skipSpaces(inputStream).maybeEol().maybeEof().noElse(); + + metricFamily.getMetricList().add(metric); + return new CharChecker(i); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/TextParser.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/TextParser.java new file mode 100644 index 0000000..8b0cb69 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/TextParser.java @@ -0,0 +1,305 @@ +/* + * 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.collector.collect.prometheus.parser; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.http.promethus.ParseException; +import org.apache.hertzbeat.common.util.StrBuffer; + +/** + * Resolves the data passed by prometheus's exporter interface http:xxx/metrics + * Reference: prometheus text_parse.go code, entry: TextToMetricFamilies + * recommend use OnlineParser + */ +@Slf4j +@Deprecated(since = "1.7.0") +public class TextParser { + private static final String NAME_LABEL = "__name__"; + private static final char LEFT_CURLY_BRACKET = '{'; + private static final char RIGHT_CURLY_BRACKET = '}'; + private static final char EQUALS = '='; + private static final char QUOTES = '"'; + private static final char ENTER = '\n'; + private static final char SPACE = ' '; + private static final char COMMA = ','; + + + /** + * parser prometheus exporter text metrics data + * @param resp txt data + * @return metrics family + */ + public static Map<String, MetricFamily> textToMetricFamilies(String resp) { + // key: metric name, value: metric family + Map<String, MetricFamily> metricMap = new ConcurrentHashMap<>(10); + try { + String[] lines = resp.split("\n"); + for (String line : lines) { + parseLine(metricMap, new StrBuffer(line)); + } + return metricMap; + } catch (Exception e) { + log.error("parse prometheus exporter data error, msg: {}", e.getMessage(), e); + } + return metricMap; + } + + private static void parseLine(Map<String, MetricFamily> metricMap, StrBuffer buffer) { + buffer.skipBlankTabs(); + if (buffer.isEmpty()) { + return; + } + switch (buffer.charAt(0)) { + case '#': + case ENTER: + break; + default: + parseMetric(metricMap, buffer); + } + } + + private static void parseMetric(Map<String, MetricFamily> metricMap, StrBuffer buffer) { + String metricName = readTokenAsMetricName(buffer); + if (metricName.isEmpty()) { + log.error("error parse metric, metric name is null, line: {}", buffer.toStr()); + return; + } + MetricFamily currentMetricFamily = metricMap.computeIfAbsent(metricName, key -> new MetricFamily()); + List<MetricFamily.Metric> metricList = currentMetricFamily.getMetricList(); + if (metricList == null) { + metricList = new ArrayList<>(); + currentMetricFamily.setMetricList(metricList); + } + MetricFamily.Metric metric = new MetricFamily.Metric(); + metricList.add(metric); + readLabels(metric, buffer); + } + + private static void readLabels(MetricFamily.Metric metric, StrBuffer buffer) { + buffer.skipBlankTabs(); + if (buffer.isEmpty()) { + return; + } + metric.setLabels(new LinkedList<>()); + if (buffer.charAt(0) == LEFT_CURLY_BRACKET) { + buffer.read(); + startReadLabelName(metric, buffer); + } else { + readLabelValue(metric, buffer); + } + } + + private static void startReadLabelName(MetricFamily.Metric metric, StrBuffer buffer) { + buffer.skipBlankTabs(); + if (buffer.isEmpty()) { + return; + } + if (buffer.charAt(0) == RIGHT_CURLY_BRACKET) { + buffer.read(); + buffer.skipBlankTabs(); + if (buffer.isEmpty()) { + return; + } + readLabelValue(metric, buffer); + return; + } + String labelName = readTokenAsLabelName(buffer); + if (labelName.isEmpty() || NAME_LABEL.equals(labelName)) { + throw new ParseException("invalid label name" + labelName + ", label name size = 0 or label name equals " + NAME_LABEL); + } + MetricFamily.Label label = new MetricFamily.Label(); + label.setName(labelName); + if (buffer.read() != EQUALS) { + throw new ParseException("parse error, not match the format of labelName=labelValue"); + } + startReadLabelValue(metric, label, buffer); + } + + private static void startReadLabelValue(MetricFamily.Metric metric, MetricFamily.Label label, StrBuffer buffer) { + buffer.skipBlankTabs(); + if (buffer.isEmpty()) { + return; + } + char c = buffer.read(); + if (c != QUOTES) { + throw new ParseException("expected '\"' at start of label value, line: " + buffer.toStr()); + } + String labelValue = readTokenAsLabelValue(buffer); + label.setValue(labelValue); + if (!isValidLabelValue(labelValue)) { + throw new ParseException("no valid label value: " + labelValue); + } + metric.getLabels().add(label); + if (buffer.isEmpty()) { + return; + } + c = buffer.read(); + switch (c) { + case COMMA -> startReadLabelName(metric, buffer); + case RIGHT_CURLY_BRACKET -> readLabelValue(metric, buffer); + default -> throw new ParseException("expected '}' or ',' at end of label value, line: " + buffer.toStr()); + } + } + + private static void readLabelValue(MetricFamily.Metric metric, StrBuffer buffer) { + buffer.skipBlankTabs(); + if (buffer.isEmpty()) { + return; + } + metric.setValue(buffer.toDouble()); + } + + /** + * Gets the name of the metric + * + * @param buffer Line data object + * @return token name + */ + private static String readTokenAsMetricName(StrBuffer buffer) { + buffer.skipBlankTabs(); + StringBuilder builder = new StringBuilder(); + if (isValidMetricNameStart(buffer.charAt(0))) { + while (!buffer.isEmpty()) { + char c = buffer.read(); + if (!isValidMetricNameContinuation(c)) { + buffer.rollback(); + break; + } + builder.append(c); + } + return builder.toString(); + } + throw new ParseException("parse metric name error"); + } + + /** + * Gets the name of the label + * + * @param buffer Line data object + * @return label name + */ + private static String readTokenAsLabelName(StrBuffer buffer) { + buffer.skipBlankTabs(); + StringBuilder builder = new StringBuilder(); + char c = buffer.read(); + if (isValidLabelNameStart(c)) { + builder.append(c); + while (!buffer.isEmpty()) { + c = buffer.read(); + if (!isValidLabelNameContinuation(c)) { + buffer.rollback(); + break; + } + builder.append(c); + } + return builder.toString(); + } + throw new ParseException("parse label name error"); + } + + /** + * Gets the value of the label + * + * @param buffer Line data object + * @return label value + */ + private static String readTokenAsLabelValue(StrBuffer buffer) { + StringBuilder builder = new StringBuilder(); + boolean escaped = false; + while (!buffer.isEmpty()) { + char c = buffer.read(); + // Handle '\\' escape character + if (escaped) { + switch (c) { + case QUOTES, '\\' -> builder.append(c); + case 'n' -> builder.append('\n'); + default -> throw new ParseException("parse label value error"); + } + escaped = false; + } else { + switch (c) { + case QUOTES: + return builder.toString(); + case ENTER: + throw new ParseException("parse label value error, next line"); + case '\\': + escaped = true; + break; + default: + builder.append(c); + } + } + } + return builder.toString(); + } + + /** + * Checks if the character complies with the metric name's first character rule + * + * @param c Metric character + * @return true/false + */ + private static boolean isValidMetricNameStart(char c) { + return isValidLabelNameStart(c) || c == ':'; + } + + /** + * Checks if the character complies with the metric name's non-first character rule + * + * @param c Metric character + * @return true/false + */ + private static boolean isValidMetricNameContinuation(char c) { + return isValidLabelNameContinuation(c) || c == ':'; + } + + /** + * Checks if the character complies with the label name's first character rule + * + * @param c Metric character + * @return true/false + */ + private static boolean isValidLabelNameStart(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; + } + + /** + * Checks if the character complies with the label name's non-first character rule + * + * @param c Metric character + * @return true/false + */ + private static boolean isValidLabelNameContinuation(char c) { + return isValidLabelNameStart(c) || (c >= '0' && c <= '9'); + } + + /** + * Checks if a string is valid UTF-8 encoded + * + * @param s label value + * @return true/false + */ + private static boolean isValidLabelValue(String s) { + return s != null; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/push/PushCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/push/PushCollectImpl.java new file mode 100644 index 0000000..d883d68 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/push/PushCollectImpl.java @@ -0,0 +1,183 @@ +/* + * 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.collector.collect.push; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.PushProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.entity.push.PushMetricsDto; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.springframework.http.MediaType; +import tools.jackson.core.type.TypeReference; + +/** + * push style collect + */ +@Slf4j +public class PushCollectImpl extends AbstractCollect { + + private static final Map<Long, Long> timeMap = new ConcurrentHashMap<>(); + + // ms + private static final Integer DEFAULT_TIMEOUT = 3000; + + private static final Integer SUCCESS_CODE = 200; + + // It's hard to determine how long ago the first data collection was, because there's no way to know when the last collection occurred. + // This makes it difficult to avoid re-collecting data after a restart. The default is 30 seconds + private static final Integer FIRST_COLLECT_INTERVAL = 30000; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getPush() == null) { + throw new IllegalArgumentException("Push collect must has Push params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, + Metrics metrics) { + long curTime = System.currentTimeMillis(); + long monitorId = builder.getId(); + PushProtocol pushProtocol = metrics.getPush(); + + Long time = timeMap.getOrDefault(monitorId, curTime - FIRST_COLLECT_INTERVAL); + timeMap.put(monitorId, curTime); + + HttpContext httpContext = createHttpContext(pushProtocol); + HttpUriRequest request = createHttpRequest(pushProtocol, monitorId, time); + + try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != SUCCESS_CODE) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode); + return; + } + String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + + parseResponse(builder, resp, metrics); + + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error(errorMsg, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_PUSH; + } + + private HttpContext createHttpContext(PushProtocol pushProtocol) { + HttpHost host = new HttpHost(pushProtocol.getHost(), Integer.parseInt(pushProtocol.getPort())); + HttpClientContext httpClientContext = new HttpClientContext(); + httpClientContext.setTargetHost(host); + return httpClientContext; + } + + private HttpUriRequest createHttpRequest(PushProtocol pushProtocol, Long monitorId, Long startTime) { + RequestBuilder requestBuilder = RequestBuilder.get(); + + + // uri + String uri = CollectUtil.replaceUriSpecialChar(pushProtocol.getUri()); + if (IpDomainUtil.isHasSchema(pushProtocol.getHost())) { + requestBuilder.setUri(pushProtocol.getHost() + ":" + pushProtocol.getPort() + uri); + } else { + String ipAddressType = IpDomainUtil.checkIpAddressType(pushProtocol.getHost()); + String baseUri = NetworkConstants.IPV6.equals(ipAddressType) + ? String.format("[%s]:%s", pushProtocol.getHost(), pushProtocol.getPort() + uri) + : String.format("%s:%s", pushProtocol.getHost(), pushProtocol.getPort() + uri); + + requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri); + } + + requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE); + requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT); + + requestBuilder.addParameter("id", String.valueOf(monitorId)); + requestBuilder.addParameter("time", String.valueOf(startTime)); + requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + + + //requestBuilder.setUri(pushProtocol.getUri()); + + if (DEFAULT_TIMEOUT > 0) { + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(DEFAULT_TIMEOUT) + .setSocketTimeout(DEFAULT_TIMEOUT) + .setRedirectsEnabled(true) + .build(); + requestBuilder.setConfig(requestConfig); + } + + return requestBuilder.build(); + } + + private void parseResponse(CollectRep.MetricsData.Builder builder, String resp, Metrics metric) { + Message<PushMetricsDto> msg = JsonUtil.fromJson(resp, new TypeReference<>() { + }); + if (msg == null) { + throw new NullPointerException("parse result is null"); + } + PushMetricsDto pushMetricsDto = msg.getData(); + if (pushMetricsDto == null || pushMetricsDto.getMetricsList() == null) { + throw new NullPointerException("parse result is null"); + } + for (PushMetricsDto.Metrics pushMetrics : pushMetricsDto.getMetricsList()) { + for (Map<String, String> metrics : pushMetrics.getMetrics()) { + List<String> metricColumn = new ArrayList<>(); + for (Metrics.Field field : metric.getFields()) { + metricColumn.add(metrics.get(field.getField())); + } + CollectRep.ValueRow valueRow = CollectRep.ValueRow.newBuilder() + .addAllColumns(metricColumn).build(); + builder.addValueRow(valueRow); + } + } + builder.setTime(System.currentTimeMillis()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/ConnectSession.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/ConnectSession.java new file mode 100644 index 0000000..38b8bb1 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/ConnectSession.java @@ -0,0 +1,27 @@ +/* + * 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.collector.collect.redfish; + +/** + * redfish client interface + */ +public interface ConnectSession extends AutoCloseable { + boolean isOpen(); + + String getRedfishResource(String uri) throws Exception; +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/RedfishClient.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/RedfishClient.java new file mode 100644 index 0000000..f9de2fd --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/RedfishClient.java @@ -0,0 +1,115 @@ +/* + * 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.collector.collect.redfish; + +import java.nio.charset.StandardCharsets; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.common.entity.job.protocol.RedfishProtocol; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpHost; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.StringEntity; +import org.springframework.http.MediaType; + +/** + * redfish client impl + */ +public class RedfishClient { + private final String host; + private final Integer port; + private final String username; + private final String password; + private final Integer timeout; + public static final String REDFISH_SESSION_SERVICE = "/redfish/v1/SessionService/Sessions"; + + protected RedfishClient(String host, int port, String username, String password, Integer timeout) { + this.host = host; + this.port = port; + this.username = username; + this.password = password; + this.timeout = timeout; + } + + public static RedfishClient create(RedfishProtocol redfishProtocol) { + return new RedfishClient(redfishProtocol.getHost(), Integer.parseInt(redfishProtocol.getPort()), + redfishProtocol.getUsername(), redfishProtocol.getPassword(), Integer.parseInt(redfishProtocol.getTimeout())); + } + + public ConnectSession connect() throws Exception { + HttpHost host = new HttpHost(this.host, this.port); + HttpClientContext httpClientContext = new HttpClientContext(); + httpClientContext.setTargetHost(host); + RequestBuilder requestBuilder = RequestBuilder.post(); + + String uri = REDFISH_SESSION_SERVICE; + if (IpDomainUtil.isHasSchema(this.host)) { + requestBuilder.setUri(this.host + ":" + this.port + uri); + } else { + String ipAddressType = IpDomainUtil.checkIpAddressType(this.host); + String baseUri = NetworkConstants.IPV6.equals(ipAddressType) + ? String.format("[%s]:%s", this.host, this.port + uri) + : String.format("%s:%s", this.host, this.port + uri); + + requestBuilder.setUri(NetworkConstants.HTTPS_HEADER + baseUri); + } + + requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE); + requestBuilder.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT); + requestBuilder.addHeader(HttpHeaders.CONTENT_ENCODING, StandardCharsets.UTF_8 + ""); + + final String json = "{\"UserName\": \"" + this.username + "\", \"Password\": \"" + this.password + "\"}"; + StringEntity entity = new StringEntity(json, StandardCharsets.UTF_8); + requestBuilder.setEntity(entity); + + if (this.timeout > 0) { + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(this.timeout) + .setSocketTimeout(this.timeout) + .setRedirectsEnabled(true) + .build(); + requestBuilder.setConfig(requestConfig); + } + + HttpUriRequest request = requestBuilder.build(); + + Session session; + try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpClientContext)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != HttpStatus.SC_CREATED) { + throw new Exception(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode); + } + String location = response.getFirstHeader(NetworkConstants.LOCATION).getValue(); + String auth = response.getFirstHeader(NetworkConstants.X_AUTH_TOKEN).getValue(); + session = new Session(auth, location, this.host, this.port); + } catch (Exception e) { + throw new Exception("Redfish session create error: " + e.getMessage()); + } finally { + request.abort(); + } + return new RedfishConnectSession(session); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/RedfishCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/RedfishCollectImpl.java new file mode 100644 index 0000000..1aa692d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/RedfishCollectImpl.java @@ -0,0 +1,189 @@ +/* + * 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.collector.collect.redfish; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection; +import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier; +import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache; +import org.apache.hertzbeat.collector.collect.redfish.cache.RedfishConnect; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.JsonPathParser; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.RedfishProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * redfish collect impl + */ +@Slf4j +public class RedfishCollectImpl extends AbstractCollect { + + private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance(); + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getRedfish() == null) { + throw new IllegalArgumentException("Redfish collect must has redfish params"); + } + RedfishProtocol redfishProtocol = metrics.getRedfish(); + Assert.hasText(redfishProtocol.getHost(), "Redfish Protocol host is required."); + Assert.hasText(redfishProtocol.getPort(), "Redfish Protocol port is required."); + Assert.hasText(redfishProtocol.getUsername(), "Redfish Protocol username is required."); + Assert.hasText(redfishProtocol.getPassword(), "Redfish Protocol password is required."); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + ConnectSession connectSession = null; + try { + connectSession = getRedfishConnectSession(metrics.getRedfish()); + } catch (Exception e) { + log.error("Redfish session create error: {}", e.getMessage()); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(e.getMessage()); + return; + } + List<String> resourcesUri = getResourcesUri(metrics, connectSession); + if (resourcesUri == null || resourcesUri.isEmpty()) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Get redfish resources uri error"); + return; + } + for (String uri : resourcesUri) { + String resp = null; + try { + resp = connectSession.getRedfishResource(uri); + } catch (Exception e) { + log.error("Get redfish {} detail resource error: {}", uri, e.getMessage()); + continue; + } + parseRedfishResource(builder, resp, metrics); + } + } + + private ConnectSession getRedfishConnectSession(RedfishProtocol redfishProtocol) throws Exception { + CacheIdentifier identifier = CacheIdentifier.builder() + .ip(redfishProtocol.getHost()) + .port(redfishProtocol.getPort()) + .password(redfishProtocol.getPassword()) + .username(redfishProtocol.getUsername()) + .build(); + ConnectSession redfishConnectSession = null; + Optional<AbstractConnection<?>> cacheOption = connectionCommonCache.getCache(identifier, true); + if (cacheOption.isPresent()) { + RedfishConnect redfishConnect = (RedfishConnect) cacheOption.get(); + redfishConnectSession = redfishConnect.getConnection(); + if (redfishConnectSession == null || !redfishConnectSession.isOpen()) { + redfishConnectSession = null; + connectionCommonCache.removeCache(identifier); + } + } + if (redfishConnectSession != null) { + return redfishConnectSession; + } + RedfishClient redfishClient = RedfishClient.create(redfishProtocol); + redfishConnectSession = redfishClient.connect(); + connectionCommonCache.addCache(identifier, new RedfishConnect(redfishConnectSession)); + return redfishConnectSession; + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_REDFISH; + } + + + private List<String> getResourcesUri(Metrics metrics, ConnectSession connectSession) { + String name = metrics.getName(); + String collectionSchema = metrics.getRedfish().getSchema(); + String schema = (collectionSchema != null) ? collectionSchema : RedfishCollectionSchema.getSchema(name); + if (!StringUtils.hasText(schema)) { + return null; + } + String pattern = "\\{\\w+\\}"; + Pattern r = Pattern.compile(pattern); + String[] fragment = r.split(schema); + List<String> res = new ArrayList<>(); + for (String value : fragment) { + List<String> temp = new ArrayList<>(); + if (res.isEmpty()) { + res.add(value); + } else { + res = res.stream().map(s -> s + value).collect(Collectors.toList()); + } + + for (String s : res) { + List<String> t = getCollectionResource(s, connectSession); + temp.addAll(t); + } + res.clear(); + res.addAll(temp); + } + return res; + } + + private List<String> parseCollectionResource(String resp) { + if (!StringUtils.hasText(resp)) { + return Collections.emptyList(); + } + String resourceIdPath = "$.Members[*].['@odata.id']"; + List<Object> resourceIds = JsonPathParser.parseContentWithJsonPath(resp, resourceIdPath); + return resourceIds.stream().filter(Objects::nonNull).map(String::valueOf).toList(); + } + + private List<String> getCollectionResource(String uri, ConnectSession connectSession) { + String resp = null; + try { + resp = connectSession.getRedfishResource(uri); + } catch (Exception e) { + log.error("Get redfish {} collection resource error: {}", uri, e.getMessage()); + return Collections.emptyList(); + } + return parseCollectionResource(resp); + } + + private void parseRedfishResource(CollectRep.MetricsData.Builder builder, String resp, Metrics metrics) { + if (!StringUtils.hasText(resp)) { + return; + } + List<String> jsonPaths = metrics.getRedfish().getJsonPath(); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String path : jsonPaths) { + List<Object> res = JsonPathParser.parseContentWithJsonPath(resp, path); + if (res != null && !res.isEmpty()) { + Object value = res.get(0); + valueRowBuilder.addColumn(value == null ? CommonConstants.NULL_VALUE : String.valueOf(value)); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + builder.addValueRow(valueRowBuilder.build()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/RedfishCollectionSchema.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/RedfishCollectionSchema.java new file mode 100644 index 0000000..5d0740e --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/RedfishCollectionSchema.java @@ -0,0 +1,37 @@ +/* + * 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.collector.collect.redfish; + +import java.util.Map; + +/** + * redfish collection schema + */ +public class RedfishCollectionSchema { + + private static final Map<String, String> schemaMap = Map.of( + "Chassis", "/redfish/v1/Chassis", + "Fan", "/redfish/v1/Chassis/{ChassisId}/ThermalSubsystem/Fans", + "Battery", "/redfish/v1/Chassis/{ChassisId}/PowerSubsystem/Batteries", + "PowerSupply", "/redfish/v1/Chassis/{ChassisId}/PowerSubsystem/PowerSupplies"); + + public static String getSchema(String key) { + return schemaMap.get(key); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/RedfishConnectSession.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/RedfishConnectSession.java new file mode 100644 index 0000000..2abc328 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/RedfishConnectSession.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.redfish; + +import java.nio.charset.StandardCharsets; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.util.EntityUtils; + +/** + * Redfish connect session + */ +public class RedfishConnectSession implements ConnectSession { + + private final Session session; + + private volatile boolean active = true; + + + public RedfishConnectSession(Session session) { + this.session = session; + } + + @Override + public boolean isOpen() { + return this.active; + } + + @Override + public void close() throws Exception { + this.active = false; + String url = RedfishClient.REDFISH_SESSION_SERVICE + session.location(); + HttpDelete httpDelete = new HttpDelete(url); + httpDelete.setHeader(NetworkConstants.X_AUTH_TOKEN, session.token()); + httpDelete.setHeader(NetworkConstants.LOCATION, session.location()); + try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(httpDelete)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != HttpStatus.SC_OK) { + throw new Exception(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode); + } + } catch (Exception e) { + throw new Exception("Redfish session close error:" + e.getMessage()); + } finally { + httpDelete.abort(); + } + } + + @Override + public String getRedfishResource(String uri) throws Exception { + if (uri.endsWith("/")) { + uri = uri.substring(0, uri.length() - 1); + } + String url = null; + if (IpDomainUtil.isHasSchema(this.session.host())) { + url = this.session.host() + ":" + this.session.port() + uri; + } else { + String ipAddressType = IpDomainUtil.checkIpAddressType(this.session.host()); + String baseUri = NetworkConstants.IPV6.equals(ipAddressType) + ? String.format("[%s]:%s", this.session.host(), this.session.port() + uri) + : String.format("%s:%s", this.session.host(), this.session.port() + uri); + url = NetworkConstants.HTTPS_HEADER + baseUri; + } + HttpGet httpGet = new HttpGet(url); + httpGet.setHeader(NetworkConstants.X_AUTH_TOKEN, session.token()); + httpGet.setHeader(NetworkConstants.LOCATION, session.location()); + try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(httpGet)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != HttpStatus.SC_OK) { + throw new Exception(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode); + } + return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + } catch (Exception e) { + throw new Exception("Redfish session get resource error:" + e.getMessage()); + } finally { + httpGet.abort(); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/Session.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/Session.java new file mode 100644 index 0000000..25d5c2e --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/Session.java @@ -0,0 +1,23 @@ +/* + * 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.collector.collect.redfish; + +/** + * Redfish session + */ +public record Session(String token, String location, String host, Integer port) {} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/cache/RedfishConnect.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/cache/RedfishConnect.java new file mode 100644 index 0000000..86d5323 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redfish/cache/RedfishConnect.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.redfish.cache; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection; +import org.apache.hertzbeat.collector.collect.redfish.ConnectSession; + +/** + * redfish connect session + */ +@Slf4j +public class RedfishConnect extends AbstractConnection<ConnectSession> { + private final ConnectSession reddishConnectSession; + + public RedfishConnect(ConnectSession reddishConnectSession) { + this.reddishConnectSession = reddishConnectSession; + } + + @Override + public void closeConnection() throws Exception { + if (reddishConnectSession != null) { + reddishConnectSession.close(); + } + } + + @Override + public ConnectSession getConnection() { + return reddishConnectSession; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redis/RedisCommonCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redis/RedisCommonCollectImpl.java new file mode 100644 index 0000000..ac520cf --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/redis/RedisCommonCollectImpl.java @@ -0,0 +1,383 @@ +/* + * 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.collector.collect.redis; + +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisConnectionException; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulConnection; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; +import io.lettuce.core.cluster.models.partitions.Partitions; +import io.lettuce.core.cluster.models.partitions.RedisClusterNode; +import io.lettuce.core.resource.ClientResources; +import io.lettuce.core.resource.DefaultClientResources; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection; +import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier; +import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache; +import org.apache.hertzbeat.collector.collect.common.cache.RedisConnect; +import org.apache.hertzbeat.collector.collect.common.ssh.SshTunnelHelper; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.RedisProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.MapCapUtil; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.channel.exception.SshChannelOpenException; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Redis single cluster collector + */ +@Slf4j +public class RedisCommonCollectImpl extends AbstractCollect { + + private static final String CLUSTER = "3"; + + private static final String SINGLE = "1"; + + private static final String CLUSTER_INFO = "cluster"; + + private static final String UNIQUE_IDENTITY = "identity"; + + private final ClientResources defaultClientResources; + private final GlobalConnectionCache connectionCache = GlobalConnectionCache.getInstance(); + + public RedisCommonCollectImpl() { + defaultClientResources = DefaultClientResources.create(); + } + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException{ + Assert.noNullElements(new Object[] {metrics, metrics.getRedis()}, "Redis collect must has redis params"); + RedisProtocol redisProtocol = metrics.getRedis(); + Assert.hasText(redisProtocol.getHost(), "Redis Protocol host is required."); + Assert.hasText(redisProtocol.getPort(), "Redis Protocol port is required."); + SshTunnelHelper.checkTunnelParam(metrics.getRedis().getSshTunnel()); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + try { + if (Objects.nonNull(metrics.getRedis().getPattern()) && Objects.equals(metrics.getRedis().getPattern(), CLUSTER)) { + List<Map<String, String>> redisInfoList = getClusterRedisInfo(metrics); + doMetricsDataList(builder, redisInfoList, metrics); + } else { + Map<String, String> redisInfo = getSingleRedisInfo(metrics); + doMetricsData(builder, redisInfo, metrics); + } + } catch (RedisConnectionException connectionException) { + String errorMsg = CommonUtil.getMessageFromThrowable(connectionException); + log.info("[redis connection] error: {}", errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (SshException sshException) { + Throwable throwable = sshException.getCause(); + if (throwable instanceof SshChannelOpenException) { + log.warn("[redis collect] Remote ssh server no more session channel, please increase sshd_config MaxSessions."); + } + String errorMsg = CommonUtil.getMessageFromThrowable(sshException); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer ssh connection failed: " + errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn("[redis collect] error: {}", e.getMessage(), e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + + /** + * get single redis metrics data + * @param metrics metrics config + * @return data + */ + private Map<String, String> getSingleRedisInfo(Metrics metrics) throws GeneralSecurityException, IOException { + StatefulRedisConnection<String, String> connection = getSingleConnection(metrics.getRedis()); + String info = connection.sync().info(metrics.getName()); + Map<String, String> valueMap = parseInfo(info, metrics); + if (log.isDebugEnabled()) { + log.debug("[RedisSingleCollectImpl] fetch redis info"); + valueMap.forEach((k, v) -> log.debug("{} : {}", k, v)); + } + return valueMap; + } + + /** + * get cluster redis metrics data + * @param metrics metrics config + * @return data + */ + private List<Map<String, String>> getClusterRedisInfo(Metrics metrics) throws GeneralSecurityException, IOException { + Map<String, StatefulRedisConnection<String, String>> connectionMap = getConnectionList(metrics.getRedis()); + List<Map<String, String>> list = new ArrayList<>(connectionMap.size()); + connectionMap.forEach((identity, connection) ->{ + String info = connection.sync().info(metrics.getName()); + Map<String, String> valueMap = parseInfo(info, metrics); + valueMap.put(UNIQUE_IDENTITY, identity); + if (Objects.equals(metrics.getName(), CLUSTER_INFO)) { + String clusterNodes = connection.sync().clusterInfo(); + valueMap.putAll(parseInfo(clusterNodes, metrics)); + } + if (log.isDebugEnabled()) { + log.debug("[RedisSingleCollectImpl] fetch redis info"); + valueMap.forEach((k, v) -> log.debug("{} : {}", k, v)); + } + list.add(valueMap); + }); + return list; + } + + /** + * Build monitoring parameters according to redis info + * @param builder builder + * @param valueMapList map list + * @param metrics metrics + */ + private void doMetricsDataList(CollectRep.MetricsData.Builder builder, List<Map<String, String>> valueMapList, Metrics metrics) { + valueMapList.forEach(e -> doMetricsData(builder, e, metrics)); + } + + /** + * Build monitoring parameters according to redis info + * @param builder builder + * @param valueMap map value + * @param metrics metrics + */ + private void doMetricsData(CollectRep.MetricsData.Builder builder, Map<String, String> valueMap, Metrics metrics) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + metrics.getAliasFields().forEach(it -> { + if (valueMap.containsKey(it)) { + String fieldValue = valueMap.get(it); + valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + }); + builder.addValueRow(valueRowBuilder.build()); + } + + /** + * get single connection + * @param redisProtocol protocol + * @return connection + */ + private StatefulRedisConnection<String, String> getSingleConnection(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException { + String[] resolvedArr = resolveHostAndPort(redisProtocol); + String host = resolvedArr[0]; + String port = resolvedArr[1]; + + CacheIdentifier identifier = doIdentifier(redisProtocol, host, port); + StatefulRedisConnection<String, String> connection = (StatefulRedisConnection<String, String>) getStatefulConnection(identifier); + if (Objects.isNull(connection)) { + // reuse connection failed, new one + RedisClient redisClient = buildSingleClient(redisProtocol, host, port); + connection = redisClient.connect(); + connectionCache.addCache(identifier, new RedisConnect(connection)); + } + return connection; + } + + /** + * get cluster connect list + * @param redisProtocol protocol + * @return connection map + */ + private Map<String, StatefulRedisConnection<String, String>> getConnectionList(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException { + // first connection + StatefulRedisClusterConnection<String, String> connection = getClusterConnection(redisProtocol); + Partitions partitions = connection.getPartitions(); + Map<String, StatefulRedisConnection<String, String>> clusterConnectionMap = new HashMap<>(partitions.size()); + for (RedisClusterNode partition : partitions) { + RedisURI uri = partition.getUri(); + RedisProtocol singleRedisProtocol = RedisProtocol.builder() + .host(uri.getHost()) + .port(String.valueOf(uri.getPort())) + .username(redisProtocol.getUsername()) + .password(redisProtocol.getPassword()) + .pattern(SINGLE) + .timeout(redisProtocol.getTimeout()) + .sshTunnel(redisProtocol.getSshTunnel()) + .build(); + StatefulRedisConnection<String, String> clusterConnection = getSingleConnection(singleRedisProtocol); + clusterConnectionMap.put(doUri(uri.getHost(), uri.getPort()), clusterConnection); + } + return clusterConnectionMap; + } + + /** + * obtain StatefulRedisClusterConnection + * + * @param redisProtocol redis protocol + * @return cluster connection + */ + private StatefulRedisClusterConnection<String, String> getClusterConnection(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException { + String[] resolvedArr = resolveHostAndPort(redisProtocol); + String host = resolvedArr[0]; + String port = resolvedArr[1]; + + CacheIdentifier identifier = doIdentifier(redisProtocol, host, port); + StatefulRedisClusterConnection<String, String> connection = (StatefulRedisClusterConnection<String, String>) getStatefulConnection(identifier); + if (connection == null) { + // reuse connection failed, new one + RedisClusterClient redisClusterClient = buildClusterClient(redisProtocol, host, port); + connection = redisClusterClient.connect(); + connectionCache.addCache(identifier, new RedisConnect(connection)); + } + return connection; + } + + /** + * get redis connection + * + * @param identifier identifier + * @return connection + */ + private StatefulConnection<String, String> getStatefulConnection(CacheIdentifier identifier) { + StatefulConnection<String, String> connection = null; + Optional<AbstractConnection<?>> cacheOption = connectionCache.getCache(identifier, true); + if (cacheOption.isPresent()) { + RedisConnect redisConnect = (RedisConnect) cacheOption.get(); + connection = redisConnect.getConnection(); + if (!connection.isOpen()) { + try { + connection.closeAsync(); + } catch (Exception e) { + log.info("The redis connect form cache, close error: {}", e.getMessage()); + } + connection = null; + connectionCache.removeCache(identifier); + } + } + return connection; + } + + /** + * build cluster redis client + * + * @param redisProtocol redis protocol config + * @return redis cluster client + */ + private RedisClusterClient buildClusterClient(RedisProtocol redisProtocol, String host, String port) { + return RedisClusterClient.create(defaultClientResources, redisUri(redisProtocol, host, port)); + } + + /** + * build single redis client + * + * @param redisProtocol redis protocol config + * @return redis single client + */ + private RedisClient buildSingleClient(RedisProtocol redisProtocol, String host, String port) { + return RedisClient.create(defaultClientResources, redisUri(redisProtocol, host, port)); + } + + private RedisURI redisUri(RedisProtocol redisProtocol, String host, String port) { + RedisURI.Builder redisUriBuilder = RedisURI.builder().withHost(host).withPort(Integer.parseInt(port)); + if (StringUtils.hasText(redisProtocol.getUsername())) { + redisUriBuilder.withClientName(redisProtocol.getUsername()); + } + if (StringUtils.hasText(redisProtocol.getPassword())) { + redisUriBuilder.withPassword(redisProtocol.getPassword().toCharArray()); + } + Duration timeout = Duration.ofMillis(CollectUtil.getTimeout(redisProtocol.getTimeout())); + redisUriBuilder.withTimeout(timeout); + return redisUriBuilder.build(); + } + + private String removeCr(String value) { + return value.replace(SignConstants.CARRIAGE_RETURN, ""); + } + + private String doUri(String ip, Integer port) { + return ip + SignConstants.DOUBLE_MARK + port; + } + + private CacheIdentifier doIdentifier(RedisProtocol redisProtocol, String host, String port) { + return CacheIdentifier.builder() + .ip(host) + .port(port) + .username(redisProtocol.getUsername()) + .password(redisProtocol.getPassword()) + .customArg(redisProtocol.getPattern()) + .build(); + } + + private Map<String, String> parseInfo(String info, Metrics metrics) { + // total of metrics configured in yml + int fieldTotalSize = metrics.getFields().size(); + String[] lines = info.split(SignConstants.LINE_FEED); + Map<String, String> result = new HashMap<>(MapCapUtil.calInitMap(fieldTotalSize)); + + Arrays.stream(lines) + .filter(it -> StringUtils.hasText(it) && !it.startsWith(SignConstants.WELL_NO) && it.contains(SignConstants.DOUBLE_MARK)) + .map(this::removeCr) + .map(r -> r.split(SignConstants.DOUBLE_MARK)) + .filter(t -> t.length > 1) + .forEach(it -> result.put(it[0], it[1])); + // fix https://github.com/apache/hertzbeat/pull/913 + if (result.size() < fieldTotalSize) { + for (Metrics.Field field : metrics.getFields()) { + if (!result.containsKey(field.getField())) { + result.put(field.getField(), CommonConstants.NULL_VALUE); + } + } + } + return result; + } + + private String[] resolveHostAndPort(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException { + boolean enableSshTunnel = Optional.ofNullable(redisProtocol.getSshTunnel()) + .map(ssh -> Boolean.parseBoolean(ssh.getEnable())) + .orElse(false); + String host; + String port; + if (enableSshTunnel){ + host = "localhost"; + port = String.valueOf(SshTunnelHelper.localPortForward(redisProtocol.getSshTunnel(), redisProtocol.getHost(), redisProtocol.getPort())); + } else { + host = redisProtocol.getHost(); + port = redisProtocol.getPort(); + } + return new String[]{host, port}; + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_REDIS; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/RegistryImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/RegistryImpl.java new file mode 100644 index 0000000..44ad119 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/RegistryImpl.java @@ -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.collector.collect.registry; + +import com.ecwid.consul.transport.TransportException; +import com.google.common.annotations.VisibleForTesting; +import java.lang.reflect.Field; +import java.util.Objects; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClient; +import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClientManagement; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServerInfo; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * registry protocol collection implementation + */ +@Slf4j +public class RegistryImpl extends AbstractCollect { + private static final String SERVER = "server"; + + @Setter + @VisibleForTesting + private DiscoveryClientManagement discoveryClientManagement = new DiscoveryClientManagement(); + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + + RegistryProtocol registryProtocol = metrics.getRegistry(); + + if (Objects.isNull(registryProtocol) || registryProtocol.isInvalid()){ + throw new IllegalArgumentException("registry collect must have a valid registry protocol param! "); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + RegistryProtocol registryProtocol = metrics.getRegistry(); + + try (DiscoveryClient discoveryClient = discoveryClientManagement.getClient(registryProtocol)) { + collectMetrics(builder, metrics, discoveryClient); + } catch (TransportException e1) { + String errorMsg = "Consul " + CommonUtil.getMessageFromThrowable(e1); + log.error(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error(errorMsg, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + + private void collectMetrics(CollectRep.MetricsData.Builder builder, Metrics metrics, DiscoveryClient discoveryClient) { + long beginTime = System.currentTimeMillis(); + // Available and Server monitor + if (StringUtils.equals(metrics.getName(), SERVER)) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + ServerInfo serverInfo = discoveryClient.getServerInfo(); + metrics.getAliasFields().forEach(fieldName -> { + if (StringUtils.equalsAnyIgnoreCase(CollectorConstants.RESPONSE_TIME, fieldName)) { + valueRowBuilder.addColumn(String.valueOf(System.currentTimeMillis() - beginTime)); + } else { + addColumnIfMatched(fieldName, serverInfo, valueRowBuilder); + } + }); + + builder.addValueRow(valueRowBuilder.build()); + } else { + // Service instances monitor + discoveryClient.getServices().forEach(serviceInstance -> { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + metrics.getAliasFields().forEach(fieldName -> addColumnIfMatched(fieldName, serviceInstance, valueRowBuilder)); + builder.addValueRow(valueRowBuilder.build()); + }); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_REGISTRY; + } + + private void addColumnIfMatched(String fieldName, Object sourceObj, CollectRep.ValueRow.Builder valueRowBuilder) { + String columnValue = null; + try { + Field declaredField = sourceObj.getClass().getDeclaredField(fieldName); + declaredField.setAccessible(Boolean.TRUE); + columnValue = String.valueOf(declaredField.get(sourceObj)); + } catch (NoSuchFieldException | IllegalAccessException e) { + log.warn("No such field for {}", fieldName); + } + + valueRowBuilder.addColumn(StringUtils.isBlank(columnValue) + ? CommonConstants.NULL_VALUE + : columnValue); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/constant/DiscoveryClientHealthStatus.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/constant/DiscoveryClientHealthStatus.java new file mode 100644 index 0000000..a4ebb6f --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/constant/DiscoveryClientHealthStatus.java @@ -0,0 +1,37 @@ +/* + * 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.collector.collect.registry.constant; + +/** + * Discovery client instance status. + */ +public final class DiscoveryClientHealthStatus { + + /** + * Discovery client instance status is UP. + */ + public static final String UP = "UP"; + + /** + * Discovery client instance status is DOWN. + */ + public static final String DOWN = "DOWN"; + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/constant/DiscoveryClientInstance.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/constant/DiscoveryClientInstance.java new file mode 100644 index 0000000..94e7980 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/constant/DiscoveryClientInstance.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.collector.collect.registry.constant; + +import java.util.Arrays; +import org.apache.commons.lang3.StringUtils; + +/** + * Discovery Client Instance Name For Http_sd monitor + */ +public enum DiscoveryClientInstance { + CONSUL("Consul"), + NACOS("Nacos"), + NOT_SUPPORT("Not support discovery client instance!"); + + private final String name; + + DiscoveryClientInstance(String name) { + this.name = name; + } + + public static DiscoveryClientInstance getByName(String clientInstanceName) { + return Arrays.stream(DiscoveryClientInstance.values()) + .filter(instance -> StringUtils.equalsIgnoreCase(instance.name, clientInstanceName)) + .findFirst() + .orElse(DiscoveryClientInstance.NOT_SUPPORT); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/DiscoveryClient.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/DiscoveryClient.java new file mode 100644 index 0000000..c27f5f3 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/DiscoveryClient.java @@ -0,0 +1,64 @@ +/* + * 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.collector.collect.registry.discovery; + +import java.util.List; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ConnectConfig; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServerInfo; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance; +import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol; + +/** + * DiscoveryClient interface. + */ +public interface DiscoveryClient extends AutoCloseable { + + /** + * Build connect config. + * @param registryProtocol registry protocol. + * @return connect config object. + */ + ConnectConfig buildConnectConfig(RegistryProtocol registryProtocol); + + /** + * Initialize client. + * @param connectConfig connect config. + */ + void initClient(ConnectConfig connectConfig); + + /** + * Get server info. + * @return server info object. + */ + ServerInfo getServerInfo(); + + /** + * Get services. + * @return service instance list. + */ + List<ServiceInstance> getServices(); + + /** + * Discovery client Health check. + * @return true if health check pass, otherwise false + */ + boolean healthCheck(); + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/DiscoveryClientManagement.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/DiscoveryClientManagement.java new file mode 100644 index 0000000..82ce789 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/DiscoveryClientManagement.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.collector.collect.registry.discovery; + +import java.util.Objects; +import org.apache.hertzbeat.collector.collect.registry.constant.DiscoveryClientInstance; +import org.apache.hertzbeat.collector.collect.registry.discovery.impl.ConsulDiscoveryClient; +import org.apache.hertzbeat.collector.collect.registry.discovery.impl.NacosDiscoveryClient; +import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol; + +/** + * Discovery Client Management + */ +public class DiscoveryClientManagement { + + public DiscoveryClient getClient(RegistryProtocol registryProtocol) { + return createClient(registryProtocol, DiscoveryClientInstance.getByName(registryProtocol.getDiscoveryClientTypeName())); + } + + private DiscoveryClient createClient(RegistryProtocol registryProtocol, DiscoveryClientInstance discoveryClientInstance) { + if (Objects.equals(discoveryClientInstance, DiscoveryClientInstance.NOT_SUPPORT)) { + return null; + } + + return doCreateClient(registryProtocol, discoveryClientInstance); + } + + private DiscoveryClient doCreateClient(RegistryProtocol registryProtocol, DiscoveryClientInstance discoveryClientInstance) { + DiscoveryClient discoveryClient; + switch (discoveryClientInstance) { + case CONSUL -> discoveryClient = new ConsulDiscoveryClient(); + case NACOS -> discoveryClient = new NacosDiscoveryClient(); + default -> { return null; } + } + discoveryClient.initClient(discoveryClient.buildConnectConfig(registryProtocol)); + return discoveryClient; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/entity/ConnectConfig.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/entity/ConnectConfig.java new file mode 100644 index 0000000..48ac9c1 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/entity/ConnectConfig.java @@ -0,0 +1,40 @@ +/* + * 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.collector.collect.registry.discovery.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +/** + * Discovery Client Connect Config + */ +@Data +@Builder +@AllArgsConstructor +public class ConnectConfig { + private String host; + private int port; + private String username; + private String password; + private String namespace; + private String serviceName; + private String groupName; +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/entity/ServerInfo.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/entity/ServerInfo.java new file mode 100644 index 0000000..7bb8548 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/entity/ServerInfo.java @@ -0,0 +1,37 @@ +/* + * 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.collector.collect.registry.discovery.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Server Info + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ServerInfo { + private String address; + private String port; +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/entity/ServiceInstance.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/entity/ServiceInstance.java new file mode 100644 index 0000000..0d0eef6 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/entity/ServiceInstance.java @@ -0,0 +1,41 @@ +/* + * 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.collector.collect.registry.discovery.entity; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +/** + * Service Instance + */ +@Data +@Builder +@AllArgsConstructor +public class ServiceInstance { + private String serviceId; + private String serviceName; + private String address; + private double weight; + private Map<String, String> metadata; + private int port; + private String healthStatus; +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/impl/ConsulDiscoveryClient.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/impl/ConsulDiscoveryClient.java new file mode 100644 index 0000000..af40ece --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/impl/ConsulDiscoveryClient.java @@ -0,0 +1,112 @@ +/* + * 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.collector.collect.registry.discovery.impl; + +import com.ecwid.consul.v1.ConsulClient; +import com.ecwid.consul.v1.agent.model.Check; +import com.ecwid.consul.v1.agent.model.Self; +import com.ecwid.consul.v1.agent.model.Service; +import com.google.common.collect.Lists; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClient; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ConnectConfig; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServerInfo; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance; +import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol; + +/** + * DiscoveryClient impl of Consul + */ +public class ConsulDiscoveryClient implements DiscoveryClient { + private ConsulClient consulClient; + + @Override + public ConnectConfig buildConnectConfig(RegistryProtocol registryProtocol) { + return ConnectConfig.builder() + .host(registryProtocol.getHost()) + .port(Integer.parseInt(registryProtocol.getPort())) + .build(); + } + + @Override + public void initClient(ConnectConfig connectConfig) { + consulClient = new ConsulClient(connectConfig.getHost(), connectConfig.getPort()); + } + + @Override + public ServerInfo getServerInfo() { + Self self = consulClient.getAgentSelf().getValue(); + return ServerInfo.builder() + .address(self.getMember().getAddress()) + .port(String.valueOf(self.getMember().getPort())) + .build(); + } + + @Override + public List<ServiceInstance> getServices() { + Map<String, Service> serviceMap = consulClient.getAgentServices().getValue(); + List<ServiceInstance> serviceInstanceList = Lists.newArrayListWithExpectedSize(serviceMap.size()); + Collection<Check> healthCheckList = consulClient.getAgentChecks().getValue().values(); + + serviceMap.forEach((serviceId, instance) -> serviceInstanceList.add(ServiceInstance.builder() + .serviceId(serviceId) + .serviceName(instance.getService()) + .address(instance.getAddress()) + .port(instance.getPort()) + .metadata(instance.getMeta()) + .healthStatus(getHealthStatus(serviceId, healthCheckList)) + .build())); + + return serviceInstanceList; + } + + @Override + public boolean healthCheck() { + + List<com.ecwid.consul.v1.health.model.Check> consulHealth = consulClient.getHealthChecksForNode("consul", null) + .getValue(); + for (com.ecwid.consul.v1.health.model.Check check : consulHealth) { + com.ecwid.consul.v1.health.model.Check.CheckStatus status = check.getStatus(); + if (status != com.ecwid.consul.v1.health.model.Check.CheckStatus.PASSING) { + return false; + } + } + + return true; + } + + @Override + public void close() { + + consulClient = null; + } + + private String getHealthStatus(String serviceId, Collection<Check> healthCheckList) { + return healthCheckList.stream() + .filter(healthCheck -> StringUtils.equals(healthCheck.getServiceId(), serviceId)) + .findFirst() + .map(check -> check.getStatus().name()) + .orElse(""); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/impl/NacosDiscoveryClient.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/impl/NacosDiscoveryClient.java new file mode 100644 index 0000000..d560bd7 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/registry/discovery/impl/NacosDiscoveryClient.java @@ -0,0 +1,179 @@ +/* + * 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.collector.collect.registry.discovery.impl; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.naming.NamingFactory; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.pojo.Instance; +import com.google.common.collect.Lists; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Properties; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.registry.constant.DiscoveryClientHealthStatus; +import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClient; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ConnectConfig; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServerInfo; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance; +import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol; +import org.springframework.util.StringUtils; + +/** + * DiscoveryClient impl of Nacos + */ +@Slf4j +public class NacosDiscoveryClient implements DiscoveryClient { + private NamingService namingService; + private ConnectConfig localConnectConfig; + + @Override + public ConnectConfig buildConnectConfig(RegistryProtocol registryProtocol) { + return ConnectConfig.builder() + .host(registryProtocol.getHost()) + .port(Integer.parseInt(registryProtocol.getPort())) + .username(registryProtocol.getUsername()) + .password(registryProtocol.getPassword()) + .namespace(registryProtocol.getNamespace()) + .serviceName(registryProtocol.getServiceName()) + .groupName(registryProtocol.getGroupName()) + .build(); + } + + @Override + public void initClient(ConnectConfig connectConfig) { + try { + + localConnectConfig = connectConfig; + Properties properties = new Properties(); + properties.put("serverAddr", connectConfig.getHost() + ":" + connectConfig.getPort()); + + if (StringUtils.hasText(connectConfig.getUsername())) { + properties.put("username", connectConfig.getUsername()); + } + if (StringUtils.hasText(connectConfig.getPassword())) { + properties.put("password", connectConfig.getPassword()); + } + if (StringUtils.hasText(connectConfig.getNamespace())) { + properties.put("namespace", connectConfig.getNamespace()); + } + + namingService = NamingFactory.createNamingService(properties); + + // Perform a synchronous probe to verify connectivity eagerly, + // because NamingFactory.createNamingService() establishes the TCP + // connection in a background thread and getServerStatus() returns + // "UP" by default before that thread finishes. + namingService.getServicesOfServer(0, 1); + } catch (NacosException exception) { + throw new RuntimeException("Failed to connect to Nacos server: " + exception.getErrMsg()); + } + } + + @Override + public ServerInfo getServerInfo() { + if (Objects.isNull(namingService)) { + throw new NullPointerException("NamingService is null"); + } + ServerInfo serverInfo; + if (healthCheck()) { + serverInfo = ServerInfo.builder() + .address(localConnectConfig.getHost()) + .port(String.valueOf(localConnectConfig.getPort())) + .build(); + } else { + throw new RuntimeException("NamingService is not healthy"); + } + + return serverInfo; + } + + @Override + public List<ServiceInstance> getServices() { + if (Objects.isNull(namingService)) { + log.error("NamingService is null"); + return Collections.emptyList(); + } + if (!healthCheck()) { + log.error("NamingService is not healthy"); + return Collections.emptyList(); + } + List<ServiceInstance> serviceInstanceList = Lists.newArrayList(); + try { + List<String> services; + + if (StringUtils.hasText(localConnectConfig.getServiceName())) { + services = List.of(localConnectConfig.getServiceName()); + } else if (StringUtils.hasText(localConnectConfig.getGroupName())) { + services = namingService.getServicesOfServer(0, 9999, localConnectConfig.getGroupName()).getData(); + } else { + services = namingService.getServicesOfServer(0, 9999).getData(); + } + + for (String serviceName : services) { + List<Instance> instances; + if (StringUtils.hasText(localConnectConfig.getGroupName())) { + instances = namingService.getAllInstances(serviceName, localConnectConfig.getGroupName()); + } else { + instances = namingService.getAllInstances(serviceName); + } + + instances.forEach(instance -> + serviceInstanceList.add(ServiceInstance.builder() + .serviceId(instance.getInstanceId()) + .serviceName(instance.getServiceName()) + .address(instance.getIp()) + .weight(instance.getWeight()) + .metadata(instance.getMetadata()) + .port(instance.getPort()) + .healthStatus(instance.isHealthy() + ? DiscoveryClientHealthStatus.UP + : DiscoveryClientHealthStatus.DOWN) + .build())); + } + } catch (NacosException e) { + throw new RuntimeException("Failed to fetch instance info"); + } + + return serviceInstanceList; + } + + @Override + public boolean healthCheck() { + + return namingService.getServerStatus().equals(DiscoveryClientHealthStatus.UP); + } + + @Override + public void close() { + if (namingService == null) { + return; + } + + try { + namingService.shutDown(); + } catch (NacosException exception) { + log.error("Nacos client close exception: {}", exception.toString()); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/s7/S7CollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/s7/S7CollectImpl.java new file mode 100644 index 0000000..30706b7 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/s7/S7CollectImpl.java @@ -0,0 +1,95 @@ +/* + * 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.collector.collect.s7; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.plc.AbstractPlcCollectImpl; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.PlcProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.S7Protocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.plc4x.java.api.PlcConnection; +import org.apache.plc4x.java.api.messages.PlcReadRequest; +import org.springframework.beans.BeanUtils; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * plc collect + */ +@Slf4j +public class S7CollectImpl extends AbstractPlcCollectImpl { + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + S7Protocol s7 = metrics.getS7(); + List<String> registerAddressList = s7.getRegisterAddresses(); + // check RackId + if (!StringUtils.hasText(s7.getRackId())) { + s7.setRackId("0"); + } + // check SlotId + if (!StringUtils.hasText(s7.getSlotId())) { + s7.setSlotId("0"); + } + // check controllerType + if (!StringUtils.hasText(s7.getControllerType())) { + s7.setControllerType("S7_1500"); + } + if (!StringUtils.hasText(s7.getTimeout())) { + s7.setTimeout("5000"); + } + PlcProtocol plc = metrics.getPlc() == null ? new PlcProtocol() : metrics.getPlc(); + plc.setRegisterAddresses(registerAddressList); + BeanUtils.copyProperties(s7, plc); + metrics.setPlc(plc); + super.preCheck(metrics); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + super.collect(builder, metrics); + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_S7; + } + + @Override + protected String getConnectionString(Metrics metrics) { + S7Protocol s7Protocol = metrics.getS7(); + return "s7://" + s7Protocol.getHost() + ":" + s7Protocol.getPort() + "?remote-rack:" + s7Protocol.getRackId() + + "&remote-slot:" + s7Protocol.getSlotId() + "&controller-type=" + s7Protocol.getControllerType(); + } + + @Override + protected PlcReadRequest buildRequest(Metrics metrics, PlcConnection connection) { + S7Protocol s7Protocol = metrics.getS7(); + List<String> registerAddressList = s7Protocol.getRegisterAddresses(); + // Create a new read request: + PlcReadRequest.Builder requestBuilder = connection.readRequestBuilder(); + for (int i = 0; i < registerAddressList.size(); i++) { + String s1 = registerAddressList.get(i); + requestBuilder.addTagAddress(metrics.getS7().getAddressSyntax() + ":" + i, s1); + } + return requestBuilder.build(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/script/ScriptCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/script/ScriptCollectImpl.java new file mode 100644 index 0000000..3d797b5 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/script/ScriptCollectImpl.java @@ -0,0 +1,264 @@ +/* + * 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.collector.collect.script; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.ScriptProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Script protocol collection implementation + */ +@Slf4j +public class ScriptCollectImpl extends AbstractCollect { + private static final String CMD = "cmd"; + private static final String BASH = "bash"; + private static final String POWERSHELL = "powershell"; + private static final String CMD_C = "/c"; + private static final String BASH_C = "-c"; + private static final String POWERSHELL_C = "-Command"; + private static final String POWERSHELL_FILE = "-File"; + private static final String PARSE_TYPE_ONE_ROW = "oneRow"; + private static final String PARSE_TYPE_MULTI_ROW = "multiRow"; + private static final String PARSE_TYPE_NETCAT = "netcat"; + private static final String PARSE_TYPE_LOG = "log"; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + Assert.notNull(metrics, "Script collect must has Imap params"); + ScriptProtocol scriptProtocol = metrics.getScript(); + Assert.notNull(scriptProtocol, "Script collect must has Imap params"); + Assert.notNull(scriptProtocol.getCharset(), "Script charset is required"); + Assert.notNull(scriptProtocol.getParseType(), "Script parse type is required"); + Assert.notNull(scriptProtocol.getScriptTool(), "Script tool is required"); + if (!(StringUtils.hasText(scriptProtocol.getScriptCommand()) || StringUtils.hasText(scriptProtocol.getScriptPath()))) { + throw new IllegalArgumentException("At least one script command or script path is required."); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + ScriptProtocol scriptProtocol = metrics.getScript(); + long startTime = System.currentTimeMillis(); + ProcessBuilder processBuilder; + // use command + if (StringUtils.hasText(scriptProtocol.getScriptCommand())) { + switch (scriptProtocol.getScriptTool()) { + case BASH -> processBuilder = new ProcessBuilder(BASH, BASH_C, scriptProtocol.getScriptCommand().trim()); + case CMD -> processBuilder = new ProcessBuilder(CMD, CMD_C, scriptProtocol.getScriptCommand().trim()); + case POWERSHELL -> processBuilder = new ProcessBuilder("powershell.exe", POWERSHELL_C, scriptProtocol.getScriptCommand().trim()); + default -> { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Not support script tool:" + scriptProtocol.getScriptTool()); + return; + } + } + // use command file + } else if (StringUtils.hasText((scriptProtocol.getScriptPath()))) { + switch (scriptProtocol.getScriptTool()) { + case BASH -> processBuilder = new ProcessBuilder(BASH, scriptProtocol.getScriptPath().trim()); + case CMD -> processBuilder = new ProcessBuilder(CMD, scriptProtocol.getScriptPath().trim()); + case POWERSHELL -> processBuilder = new ProcessBuilder(POWERSHELL, POWERSHELL_FILE, scriptProtocol.getScriptPath().trim()); + default -> { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Not support script tool:" + scriptProtocol.getScriptTool()); + return; + } + } + } else { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("At least one script command or script path is required."); + return; + } + // set work directory + String workDirectory = scriptProtocol.getWorkDirectory(); + if (StringUtils.hasText(workDirectory)) { + processBuilder.directory(new File(workDirectory)); + } + // execute command + try { + Process process = processBuilder.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName(scriptProtocol.getCharset()))); + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + if (StringUtils.hasText(line)) { + response.append(line).append("\n"); + } + } + process.waitFor(); + Long responseTime = System.currentTimeMillis() - startTime; + String result = String.valueOf(response); + if (!StringUtils.hasText(result)) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Script response data is null"); + return; + } + switch (scriptProtocol.getParseType()) { + case PARSE_TYPE_LOG -> parseResponseDataByLog(result, metrics.getAliasFields(), builder, responseTime); + case PARSE_TYPE_NETCAT -> parseResponseDataByNetcat(result, metrics.getAliasFields(), builder, responseTime); + case PARSE_TYPE_ONE_ROW -> parseResponseDataByOne(result, metrics.getAliasFields(), builder, responseTime); + case PARSE_TYPE_MULTI_ROW -> parseResponseDataByMulti(result, metrics.getAliasFields(), builder, responseTime); + default -> { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Script collect not support this parse type: " + scriptProtocol.getParseType()); + } + } + } catch (IOException ioException) { + String errorMsg = CommonUtil.getMessageFromThrowable(ioException); + log.warn(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Peer io failed: " + errorMsg); + } catch (InterruptedException interruptedException){ + String errorMsg = CommonUtil.getMessageFromThrowable(interruptedException); + log.warn(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Peer interrupt this script: " + errorMsg); + } catch (Exception exception) { + String errorMsg = CommonUtil.getMessageFromThrowable(exception); + log.warn(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_SCRIPT; + } + + private void parseResponseDataByLog(String result, List<String> aliasFields, CollectRep.MetricsData.Builder builder, Long responseTime) { + String[] lines = result.split("\n"); + if (lines.length + 1 < aliasFields.size()) { + log.error("Response data not enough: {}", result); + return; + } + for (String line : lines) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(responseTime.toString()); + } else { + valueRowBuilder.addColumn(line); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + + private void parseResponseDataByNetcat(String result, List<String> aliasFields, CollectRep.MetricsData.Builder builder, Long responseTime) { + String[] lines = result.split("\n"); + if (lines.length + 1 < aliasFields.size()) { + log.error("ssh response data not enough: {}", result); + return; + } + boolean contains = lines[0].contains("="); + Map<String, String> mapValue = Arrays.stream(lines) + .map(item -> { + if (contains) { + return item.split("="); + } else { + return item.split("\t"); + } + }) + .filter(item -> item.length == 2) + .collect(Collectors.toMap(x -> x[0], x -> x[1])); + + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String field : aliasFields) { + String fieldValue = mapValue.get(field); + valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } + builder.addValueRow(valueRowBuilder.build()); + } + + private void parseResponseDataByOne(String result, List<String> aliasFields, CollectRep.MetricsData.Builder builder, Long responseTime) { + String[] lines = result.split("\n"); + if (lines.length + 1 < aliasFields.size()) { + log.error("ssh response data not enough: {}", result); + return; + } + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + int aliasIndex = 0; + int lineIndex = 0; + while (aliasIndex < aliasFields.size()) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(aliasFields.get(aliasIndex))) { + valueRowBuilder.addColumn(responseTime.toString()); + } else { + if (lineIndex < lines.length) { + valueRowBuilder.addColumn(lines[lineIndex].trim()); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + lineIndex++; + } + aliasIndex++; + } + builder.addValueRow(valueRowBuilder.build()); + } + + private void parseResponseDataByMulti(String result, List<String> aliasFields, + CollectRep.MetricsData.Builder builder, Long responseTime) { + String[] lines = result.split("\n"); + if (lines.length <= 1) { + log.error("ssh response data only has header: {}", result); + return; + } + String[] fields = lines[0].split("\\s+"); + Map<String, Integer> fieldMapping = new HashMap<>(fields.length); + for (int i = 0; i < fields.length; i++) { + fieldMapping.put(fields[i].trim().toLowerCase(), i); + } + for (int i = 1; i < lines.length; i++) { + String[] values = lines[i].split("\\s+"); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(responseTime.toString()); + } else { + Integer index = fieldMapping.get(alias.toLowerCase()); + if (index != null && index < values.length) { + valueRowBuilder.addColumn(values[index]); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/ConsulSdCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/ConsulSdCollectImpl.java new file mode 100644 index 0000000..5a52469 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/ConsulSdCollectImpl.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.sd; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.registry.constant.DiscoveryClientInstance; +import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClient; +import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClientManagement; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.ConsulSdProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.springframework.util.CollectionUtils; + +import java.util.List; + +/** + * consul sd collector + */ +@Slf4j +public class ConsulSdCollectImpl extends AbstractCollect { + + private static final DiscoveryClientManagement discoveryClientManagement = new DiscoveryClientManagement(); + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + ConsulSdProtocol consulSd = metrics.getConsul_sd(); + if (consulSd == null || consulSd.isInvalid()) { + throw new IllegalArgumentException("Consul Service Discovery params is required."); + } + + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + RegistryProtocol registryProtocol = RegistryProtocol.builder() + .host(metrics.getConsul_sd().getHost()) + .port(metrics.getConsul_sd().getPort()) + .discoveryClientTypeName(DiscoveryClientInstance.CONSUL.name()) + .build(); + try (DiscoveryClient client = discoveryClientManagement.getClient(registryProtocol)){ + List<ServiceInstance> services = client.getServices(); + if (CollectionUtils.isEmpty(services)) { + return; + } + services.forEach(instance -> { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + valueRowBuilder.addColumn(instance.getAddress()); + valueRowBuilder.addColumn(String.valueOf(instance.getPort())); + builder.addValueRow(valueRowBuilder.build()); + }); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn("Failed to fetch consul sd... {}", errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_CONSUL_SD; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/DnsSdCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/DnsSdCollectImpl.java new file mode 100644 index 0000000..a9c427f --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/DnsSdCollectImpl.java @@ -0,0 +1,181 @@ +/* + * 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.collector.collect.sd; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.xbill.DNS.AAAARecord; +import org.xbill.DNS.ARecord; +import org.xbill.DNS.Lookup; +import org.xbill.DNS.MXRecord; +import org.xbill.DNS.NSRecord; +import org.xbill.DNS.Record; +import org.xbill.DNS.SRVRecord; +import org.xbill.DNS.SimpleResolver; +import org.xbill.DNS.TextParseException; +import org.xbill.DNS.Type; + +import java.time.Duration; +import java.util.Arrays; + +/** + * DNS SD collector supporting multiple record types + */ +@Slf4j +public class DnsSdCollectImpl extends AbstractCollect { + + private static final int DEFAULT_TIME_OUT = 3000; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics.getDns_sd() == null) { + throw new IllegalArgumentException("DNS SD configuration cannot be null"); + } + if (metrics.getDns_sd().getHost() == null || metrics.getDns_sd().getHost().isEmpty()) { + throw new IllegalArgumentException("DNS host cannot be null or empty"); + } + if (metrics.getDns_sd().getPort() == null || metrics.getDns_sd().getPort().isEmpty()) { + throw new IllegalArgumentException("DNS port cannot be null or empty"); + } + if (metrics.getDns_sd().getRecordType() == null || metrics.getDns_sd().getRecordType().isEmpty()) { + throw new IllegalArgumentException("DNS record type cannot be null or empty"); + } + if (metrics.getDns_sd().getRecordName() == null || metrics.getDns_sd().getRecordName().isEmpty()) { + throw new IllegalArgumentException("DNS record name cannot be null or empty"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + String hostName = metrics.getDns_sd().getHost(); + int type = Integer.parseInt(metrics.getDns_sd().getRecordType()); + Type.check(type); + String recordName = metrics.getDns_sd().getRecordName(); + try { + Lookup lookup = new Lookup(recordName, type); + SimpleResolver resolver = new SimpleResolver(metrics.getDns_sd().getHost()); + resolver.setPort(Integer.parseInt(metrics.getDns_sd().getPort())); + resolver.setTimeout(Duration.ofMillis(DEFAULT_TIME_OUT)); + lookup.setResolver(resolver); + lookup.setCache(null); + lookup.run(); + if (lookup.getResult() != Lookup.SUCCESSFUL) { + String msg = String.format("DNS lookup failed for: %s, error: %s", recordName, lookup.getErrorString()); + log.warn(msg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(msg); + return; + } + Record[] records = lookup.getAnswers(); + if (records == null || records.length == 0) { + log.info("No record type: {} records found for host: {}", type, hostName); + builder.setCode(CollectRep.Code.SUCCESS); + return; + } + processRecords(builder, records, type); + } catch (TextParseException e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn("Failed to parse dns query... {}", errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error("Failed to fetch dns sd...{}", errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + + private void processRecords(CollectRep.MetricsData.Builder builder, Record[] records, int recordType) { + switch (recordType) { + case Type.A: + processARecords(builder, records); + break; + case Type.AAAA: + processAAAARecords(builder, records); + break; + case Type.SRV: + processSrvRecords(builder, records); + break; + case Type.MX: + processMxRecords(builder, records); + break; + case Type.NS: + processNsRecords(builder, records); + break; + default: + throw new IllegalStateException("Invalid record type: " + recordType); + } + } + + @SuppressWarnings({"checkstyle:AbbreviationAsWordInName", "checkstyle:LambdaParameterName"}) + private void processARecords(CollectRep.MetricsData.Builder builder, Record[] records) { + Arrays.stream(records).filter(ARecord.class::isInstance).map(ARecord.class::cast).forEach(aRecord -> { + CollectRep.ValueRow.Builder row = CollectRep.ValueRow.newBuilder(); + row.addColumn(aRecord.getAddress().getHostAddress()); + row.addColumn(""); //A record has no port + builder.addValueRow(row.build()); + }); + } + + @SuppressWarnings("checkstyle:AbbreviationAsWordInName") + private void processAAAARecords(CollectRep.MetricsData.Builder builder, Record[] records) { + Arrays.stream(records).filter(AAAARecord.class::isInstance).map(AAAARecord.class::cast).forEach(aaaaRecord -> { + CollectRep.ValueRow.Builder row = CollectRep.ValueRow.newBuilder(); + row.addColumn(aaaaRecord.getAddress().getHostAddress()); + row.addColumn(""); //AAAA record has no port + builder.addValueRow(row.build()); + }); + } + + private void processSrvRecords(CollectRep.MetricsData.Builder builder, Record[] records) { + Arrays.stream(records).filter(SRVRecord.class::isInstance).map(SRVRecord.class::cast).forEach(srvRecord -> { + CollectRep.ValueRow.Builder row = CollectRep.ValueRow.newBuilder(); + row.addColumn(srvRecord.getTarget().toString(true)); + row.addColumn(String.valueOf(srvRecord.getPort())); + builder.addValueRow(row.build()); + }); + } + + private void processMxRecords(CollectRep.MetricsData.Builder builder, Record[] records) { + Arrays.stream(records).filter(MXRecord.class::isInstance).map(MXRecord.class::cast).forEach(mxRecord -> { + CollectRep.ValueRow.Builder row = CollectRep.ValueRow.newBuilder(); + row.addColumn(mxRecord.getTarget().toString(true)); + row.addColumn(""); //MX record has no port + builder.addValueRow(row.build()); + }); + } + + private void processNsRecords(CollectRep.MetricsData.Builder builder, Record[] records) { + Arrays.stream(records).filter(NSRecord.class::isInstance).map(NSRecord.class::cast).forEach(nsRecord -> { + CollectRep.ValueRow.Builder row = CollectRep.ValueRow.newBuilder(); + row.addColumn(nsRecord.getTarget().toString(true)); + row.addColumn(""); //NS record has no port + builder.addValueRow(row.build()); + }); + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_DNS_SD; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/EurekaSdCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/EurekaSdCollectImpl.java new file mode 100644 index 0000000..c7f457c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/EurekaSdCollectImpl.java @@ -0,0 +1,111 @@ +/* + * 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.collector.collect.sd; + +import com.google.common.collect.Lists; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.EurekaSdProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.entity.sd.ConnectionConfig; +import org.apache.hertzbeat.common.entity.sd.EurekaDiscoveryResponseEntity; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.XmlUtil; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.util.EntityUtils; +import org.springframework.util.CollectionUtils; + +/** + * eureka service discovery collect impl + */ +@Slf4j +public class EurekaSdCollectImpl extends AbstractCollect { + + private static final String APP_LIST_PATH = "/apps"; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + EurekaSdProtocol eurekaSd = metrics.getEureka_sd(); + if (eurekaSd == null || eurekaSd.getUrl() == null) { + throw new IllegalArgumentException("Eureka Service Discovery url is required."); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + List<ConnectionConfig> configList = Lists.newArrayList(); + HttpUriRequest request = RequestBuilder.get().setUri(metrics.getEureka_sd().getUrl() + APP_LIST_PATH).build(); + try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != 200) { + log.warn("Failed to fetch eureka sd..."); + builder.setMsg("StatusCode " + statusCode); + builder.setCode(CollectRep.Code.FAIL); + return; + } + String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + EurekaDiscoveryResponseEntity responseEntity = + XmlUtil.fromXml(responseBody, EurekaDiscoveryResponseEntity.class); + if (responseEntity == null || CollectionUtils.isEmpty(responseEntity.getApplications())) { + return; + } + + responseEntity.getApplications() + .stream() + .filter(application -> !CollectionUtils.isEmpty(application.getInstances())) + .forEach(application -> convertTarget(configList, application)); + + configList.forEach(config -> { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + valueRowBuilder.addColumn(config.getHost()); + valueRowBuilder.addColumn(config.getPort()); + builder.addValueRow(valueRowBuilder.build()); + }); + } catch (IOException e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn("Failed to fetch eureka sd... {}", errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + + private void convertTarget(List<ConnectionConfig> configList, + EurekaDiscoveryResponseEntity.Application application) { + application.getInstances().forEach(instance -> { + String host = instance.getIpAddr(); + int port = instance.getPort() == null ? 80 : instance.getPort(); + configList.add(ConnectionConfig.builder() + .host(host) + .port(String.valueOf(port)) + .build()); + }); + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_EUREKA_SD; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/HttpSdCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/HttpSdCollectImpl.java new file mode 100644 index 0000000..da4ff81 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/HttpSdCollectImpl.java @@ -0,0 +1,242 @@ +/* + * 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.collector.collect.sd; + +import tools.jackson.core.type.TypeReference; +import com.google.common.collect.Lists; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.entity.sd.ConnectionConfig; +import org.apache.hertzbeat.common.entity.sd.ServiceDiscoveryResponseEntity; +import org.apache.hertzbeat.common.util.Base64Util; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.http.HttpHeaders; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.springframework.http.MediaType; +import org.springframework.util.CollectionUtils; +import org.springframework.web.util.UriUtils; + + +/** + * http sd collector + */ +@Slf4j +public class HttpSdCollectImpl extends AbstractCollect { + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + List<ConnectionConfig> configList = Lists.newArrayList(); + + HttpUriRequest request = createHttpRequest(metrics.getHttp_sd()); + HttpContext httpContext = createHttpContext(metrics.getHttp_sd()); + try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != 200) { + log.warn("Failed to fetch sd..."); + builder.setMsg("StatusCode " + statusCode); + builder.setCode(CollectRep.Code.FAIL); + return; + } + + String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + TypeReference<List<ServiceDiscoveryResponseEntity>> typeReference = new TypeReference<>() {}; + final List<ServiceDiscoveryResponseEntity> responseEntityList = JsonUtil.fromJson(responseBody, typeReference); + if (CollectionUtils.isEmpty(responseEntityList)) { + return; + } + + responseEntityList.stream() + .filter(entity -> !CollectionUtils.isEmpty(entity.getTarget())) + .forEach(responseEntity -> convertTarget(configList, responseEntity)); + + + configList.forEach(config -> { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + valueRowBuilder.addColumn(config.getHost()); + valueRowBuilder.addColumn(config.getPort()); + builder.addValueRow(valueRowBuilder.build()); + }); + } catch (IOException e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn("Failed to fetch sd... {}", errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + + private void convertTarget(List<ConnectionConfig> configList, ServiceDiscoveryResponseEntity responseEntity) { + responseEntity.getTarget().stream() + .filter(StringUtils::isNotBlank) + .forEach(fetchedTarget -> addConfig(configList, fetchedTarget)); + } + + private void addConfig(List<ConnectionConfig> configList, String fetchedTarget) { + for (String url : fetchedTarget.split(",")) { + final String[] split = url.split(":"); + if (split.length != 2) { + continue; + } + + configList.add(ConnectionConfig.builder() + .host(split[0]) + .port(split[1]) + .build()); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_HTTP_SD; + } + + /** + * create httpContext + * + * @param httpSdProtocol http sd protocol + * @return context + */ + public HttpContext createHttpContext(HttpProtocol httpSdProtocol) { + HttpProtocol.Authorization auth = httpSdProtocol.getAuthorization(); + if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) { + HttpClientContext clientContext = new HttpClientContext(); + if (org.springframework.util.StringUtils.hasText(auth.getDigestAuthUsername()) + && org.springframework.util.StringUtils.hasText(auth.getDigestAuthPassword())) { + CredentialsProvider provider = new BasicCredentialsProvider(); + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(auth.getDigestAuthUsername(), + auth.getDigestAuthPassword()); + URL url; + try { + url = new URL(httpSdProtocol.getUrl()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid URI format.", e); + } + int port = url.getPort() != -1 ? url.getPort() : ("https".equals(url.getProtocol()) ? 443 : 80); + AuthScope authScope = new AuthScope(url.getHost(), port); + provider.setCredentials(authScope, credentials); + + clientContext.setCredentialsProvider(provider); + return clientContext; + } + } + return null; + } + + /** + * create http request + * + * @param httpSdProtocol http request set + * @return http uri request + */ + public HttpUriRequest createHttpRequest(HttpProtocol httpSdProtocol) { + RequestBuilder requestBuilder = RequestBuilder.get(); + + // The default request header can be overridden if customized + // keep-alive + requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE); + requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT); + // headers The custom request header is overwritten here + Map<String, String> headers = httpSdProtocol.getHeaders(); + if (headers != null && !headers.isEmpty()) { + for (Map.Entry<String, String> header : headers.entrySet()) { + if (org.springframework.util.StringUtils.hasText(header.getValue())) { + requestBuilder.addHeader(header.getKey(), header.getValue()); + } + } + } + // add accept + requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + + // add authorization + if (httpSdProtocol.getAuthorization() != null) { + HttpProtocol.Authorization authorization = httpSdProtocol.getAuthorization(); + if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) { + String value = DispatchConstants.BEARER + SignConstants.BLANK + authorization.getBearerTokenToken(); + requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value); + } else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) { + if (org.springframework.util.StringUtils.hasText(authorization.getBasicAuthUsername()) + && org.springframework.util.StringUtils.hasText(authorization.getBasicAuthPassword())) { + String authStr = authorization.getBasicAuthUsername() + SignConstants.DOUBLE_MARK + authorization.getBasicAuthPassword(); + String encodedAuth = Base64Util.encode(authStr); + requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + SignConstants.BLANK + encodedAuth); + } + } + } + + // uri encode, default true + boolean enableUrlEncoding = Boolean.parseBoolean(httpSdProtocol.getEnableUrlEncoding()); + if (enableUrlEncoding) { + // if the url contains parameters directly + if (httpSdProtocol.getUrl().contains("?")) { + String path = httpSdProtocol.getUrl().substring(0, httpSdProtocol.getUrl().indexOf("?")); + String query = httpSdProtocol.getUrl().substring(httpSdProtocol.getUrl().indexOf("?") + 1); + httpSdProtocol.setUrl(UriUtils.encodePath(path, "UTF-8") + "?" + UriUtils.encodeQuery(query, "UTF-8")); + } else { + httpSdProtocol.setUrl(UriUtils.encodePath(httpSdProtocol.getUrl(), "UTF-8")); + } + } + + // set uri + try { + requestBuilder.setUri(httpSdProtocol.getUrl()); + } catch (IllegalArgumentException e) { + log.warn("Invalid URI with illegal characters: {}. User has disabled URL encoding, not applying any encoding.", httpSdProtocol.getUrl()); + throw e; + } + + // custom timeout + int timeout = CollectUtil.getTimeout(httpSdProtocol.getTimeout(), 0); + if (timeout > 0) { + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(timeout) + .setSocketTimeout(timeout) + .setRedirectsEnabled(true) + .build(); + requestBuilder.setConfig(requestConfig); + } + return requestBuilder.build(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/NacosSdCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/NacosSdCollectImpl.java new file mode 100644 index 0000000..4ba07ce --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/NacosSdCollectImpl.java @@ -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.collector.collect.sd; + +import java.util.List; + +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.registry.constant.DiscoveryClientInstance; +import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClient; +import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClientManagement; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.springframework.util.CollectionUtils; + +import lombok.extern.slf4j.Slf4j; + +/** + * Nacos Service Discovery Collector + * This collector integrates with Nacos to discover registered services + */ +@Slf4j +public class NacosSdCollectImpl extends AbstractCollect { + + /** + * Client management to interact with discovery services + */ + private final DiscoveryClientManagement discoveryClientManagement = new DiscoveryClientManagement(); + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + // Validate the required configuration is present + if (metrics == null || metrics.getNacos_sd() == null) { + throw new IllegalArgumentException("Nacos service discovery monitoring, the config is null"); + } + if (metrics.getNacos_sd().isInvalid()) { + throw new IllegalArgumentException("Nacos service discovery monitoring, the config is invalid"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + // Create Registry protocol from NacosSd protocol + RegistryProtocol registryProtocol = RegistryProtocol.builder() + .host(metrics.getNacos_sd().getHost()) + .port(metrics.getNacos_sd().getPort()) + .username(metrics.getNacos_sd().getUsername()) + .password(metrics.getNacos_sd().getPassword()) + .namespace(metrics.getNacos_sd().getNamespace()) + .groupName(metrics.getNacos_sd().getGroupName()) + .serviceName(metrics.getNacos_sd().getServiceName()) + .discoveryClientTypeName(DiscoveryClientInstance.NACOS.name()) + .build(); + + DiscoveryClient discoveryClient = null; + try { + // Use the existing NacosDiscoveryClient through DiscoveryClientManagement + discoveryClient = discoveryClientManagement.getClient(registryProtocol); + if (discoveryClient == null) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Failed to get Nacos discovery client"); + return; + } + + // Get all services registered in Nacos + List<ServiceInstance> services = discoveryClient.getServices(); + if (CollectionUtils.isEmpty(services)) { + return; + } + + // Populate the response data with service information + services.forEach(service -> { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + valueRowBuilder.addColumn(service.getAddress()); + valueRowBuilder.addColumn(String.valueOf(service.getPort())); + valueRowBuilder.addColumn(service.getServiceName()); + valueRowBuilder.addColumn(service.getHealthStatus()); + builder.addValueRow(valueRowBuilder.build()); + }); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn("Failed to fetch services from Nacos: {}", errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + // Close the discovery client to release resources + if (discoveryClient != null) { + try { + discoveryClient.close(); + } catch (Exception e) { + log.warn("Failed to close Nacos discovery client: {}", CommonUtil.getMessageFromThrowable(e)); + } + } + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_NACOS_SD; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/ZookeeperSdCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/ZookeeperSdCollectImpl.java new file mode 100644 index 0000000..57a0068 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/sd/ZookeeperSdCollectImpl.java @@ -0,0 +1,95 @@ +/* + * 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.collector.collect.sd; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.entity.job.protocol.ZookeeperSdProtocol; +import org.apache.hertzbeat.common.entity.sd.ConnectionConfig; +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.ZooKeeper; +import org.apache.hertzbeat.common.util.CommonUtil; +import java.io.IOException; +import java.util.List; + +/** + * zookeeper sd collector + */ +@Slf4j +public class ZookeeperSdCollectImpl extends AbstractCollect { + + private static int TIMEOUT = 30000; + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + ZookeeperSdProtocol zookeeperSdProtocol = metrics.getZookeeper_sd(); + String url = zookeeperSdProtocol.getUrl(); + String pathPrefix = zookeeperSdProtocol.getPathPrefix(); + try (ZooKeeper zk = new ZooKeeper(url, TIMEOUT, event -> {})){ + + List<String> children = zk.getChildren(pathPrefix, false); + List<ConnectionConfig> connectionConfigs = children.stream().map(node -> { + String[] split = node.split(":"); + if (split.length != 2) log.warn("Node format is incorrect: {}, expected format is 'host:port'", node); + return new ConnectionConfig(split[0], split[1]); + }).toList(); + connectionConfigs.forEach(config -> { + CollectRep.ValueRow valueRow = CollectRep.ValueRow.newBuilder() + .addColumn(config.getHost()) + .addColumn(config.getPort()) + .build(); + builder.addValueRow(valueRow); + }); + } catch (IOException e){ + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error("Failed to connect to Zookeeper: {}", errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } catch (InterruptedException e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error("Zookeeper connection interrupted: {}", errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } catch (KeeperException e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error("Zookeeper operation failed: {}", errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics.getZookeeper_sd() == null) { + throw new IllegalArgumentException("Zookeeper SD configuration cannot be null"); + } + if (metrics.getZookeeper_sd().getUrl() == null || metrics.getZookeeper_sd().getUrl().isEmpty()) { + throw new IllegalArgumentException("Zookeeper URL cannot be null or empty"); + } + if (metrics.getZookeeper_sd().getPathPrefix() == null || metrics.getZookeeper_sd().getPathPrefix().isEmpty()) { + throw new IllegalArgumentException("Zookeeper path prefix cannot be null or empty"); + } + } + + @Override + public String supportProtocol() { + return "zookeeper_sd"; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/smtp/SmtpCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/smtp/SmtpCollectImpl.java new file mode 100644 index 0000000..8e4e923 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/smtp/SmtpCollectImpl.java @@ -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.collector.collect.smtp; + +import java.io.IOException; +import java.net.SocketException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.net.smtp.SMTP; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.SmtpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * smtp collect + */ +@Slf4j +public class SmtpCollectImpl extends AbstractCollect { + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getSmtp() == null) { + throw new IllegalArgumentException("Smtp collect must has Smtp params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + SmtpProtocol smtpProtocol = metrics.getSmtp(); + String host = smtpProtocol.getHost(); + String port = smtpProtocol.getPort(); + int timeout = CollectUtil.getTimeout(smtpProtocol.getTimeout()); + SMTP smtp = null; + try { + smtp = new SMTP(); + smtp.setConnectTimeout(timeout); + smtp.setCharset(StandardCharsets.UTF_8); + smtp.connect(host, Integer.parseInt(port)); + if (smtp.isConnected()) { + long responseTime = System.currentTimeMillis() - startTime; + List<String> aliasFields = metrics.getAliasFields(); + + Map<String, String> resultMap = execCmdAndParseResult(smtp, smtpProtocol.getCmd(), smtpProtocol); + resultMap.put(CollectorConstants.RESPONSE_TIME, Long.toString(responseTime)); + if (resultMap.size() < aliasFields.size()) { + log.error("smtp response data not enough: {}", resultMap); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("The cmd execution results do not match the expected number of metrics."); + return; + } + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String field : aliasFields) { + String fieldValue = resultMap.get(field); + valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } + builder.addValueRow(valueRowBuilder.build()); + } else { + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer connect failed,Timeout " + timeout + "ms"); + return; + } + smtp.disconnect(); + } catch (SocketException socketException) { + String errorMsg = CommonUtil.getMessageFromThrowable(socketException); + log.debug(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("The peer refused to connect: service port does not listening or firewall: " + errorMsg); + } catch (IOException ioException) { + String errorMsg = CommonUtil.getMessageFromThrowable(ioException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer connect failed: " + errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn(errorMsg, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (smtp != null) { + try { + smtp.disconnect(); + } catch (Exception e) { + log.warn(e.getMessage()); + } + } + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_SMTP; + } + + private static Map<String, String> execCmdAndParseResult(SMTP smtp, String cmd, SmtpProtocol smtpProtocol) throws IOException { + Map<String, String> result = new HashMap<>(8); + // Store the response of the SMTP connection + result.put("smtpBanner", smtp.getReplyString()); + smtp.helo(smtpProtocol.getEmail()); + // Retrieve the response for the HELO command + String replyString = smtp.getReplyString(); + result.put("heloInfo", replyString); + String[] lines = replyString.split("\n"); + for (String line : lines) { + if (line.startsWith("250")) { + result.put("response", "OK"); + } + } + return result; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/snmp/SnmpCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/snmp/SnmpCollectImpl.java new file mode 100644 index 0000000..4c12a2c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/snmp/SnmpCollectImpl.java @@ -0,0 +1,413 @@ +/* + * 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.collector.collect.snmp; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.HexFormat; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.SnmpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.snmp4j.PDU; +import org.snmp4j.Snmp; +import org.snmp4j.Target; +import org.snmp4j.fluent.SnmpBuilder; +import org.snmp4j.fluent.SnmpCompletableFuture; +import org.snmp4j.fluent.TargetBuilder; +import org.snmp4j.mp.MPv3; +import org.snmp4j.mp.SnmpConstants; +import org.snmp4j.security.AuthMD5; +import org.snmp4j.security.PrivDES; +import org.snmp4j.security.SecurityModel; +import org.snmp4j.security.SecurityModels; +import org.snmp4j.security.SecurityProtocols; +import org.snmp4j.security.USM; +import org.snmp4j.security.UsmUser; +import org.snmp4j.smi.Address; +import org.snmp4j.smi.GenericAddress; +import org.snmp4j.smi.OID; +import org.snmp4j.smi.OctetString; +import org.snmp4j.smi.TimeTicks; +import org.snmp4j.smi.Variable; +import org.snmp4j.smi.VariableBinding; +import org.snmp4j.util.DefaultPDUFactory; +import org.snmp4j.util.TableEvent; +import org.snmp4j.util.TableUtils; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Snmp protocol collection implementation + */ +@Slf4j +public class SnmpCollectImpl extends AbstractCollect { + + private static final String AES128 = "1"; + private static final String SHA1 = "1"; + private static final String DEFAULT_PROTOCOL = "udp"; + private static final String OPERATION_GET = "get"; + private static final String OPERATION_WALK = "walk"; + private static final String HEX_SPLIT = ":"; + private static final String FORMAT_PATTERN = + "{0,choice,0#|1#1 day, |1<{0,number,integer} days, }" + + "{1,choice,0#|1#1 hour, |1<{1,number,integer} hours, }" + + "{2,choice,0#|1#1 minute, |1<{2,number,integer} minutes, }" + + "{3,choice,0#|1#1 second, |1<{3,number,integer} seconds }"; + private static final String DATE_AND_TIME_PATTERN = "%04d-%02d-%02d,%02d:%02d:%02d.%d"; + private static final String TIME_ZONE_PATTERN = "%c%02d:%02d"; + + private final Map<Integer, Snmp> versionSnmpService = new ConcurrentHashMap<>(3); + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getSnmp() == null) { + throw new IllegalArgumentException("Snmp collect must has snmp params"); + } + SnmpProtocol snmpProtocol = metrics.getSnmp(); + Assert.hasText(snmpProtocol.getHost(), "snmp host is required."); + Assert.hasText(snmpProtocol.getPort(), "snmp port is required."); + Assert.notNull(snmpProtocol.getVersion(), "snmp version is required."); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + SnmpProtocol snmpProtocol = metrics.getSnmp(); + int timeout = CollectUtil.getTimeout(snmpProtocol.getTimeout()); + int snmpVersion = getSnmpVersion(snmpProtocol.getVersion()); + Snmp snmpService = null; + try { + SnmpBuilder snmpBuilder = new SnmpBuilder(); + snmpService = getSnmpService(snmpVersion, snmpBuilder); + snmpService.listen(); + Target<?> target; + Address targetAddress = GenericAddress.parse(DEFAULT_PROTOCOL + ":" + snmpProtocol.getHost() + + "/" + snmpProtocol.getPort()); + TargetBuilder<?> targetBuilder = snmpBuilder.target(targetAddress); + if (snmpVersion == SnmpConstants.version3) { + TargetBuilder.PrivProtocol privatePasswordEncryption = getPrivPasswordEncryption(snmpProtocol.getPrivPasswordEncryption()); + TargetBuilder.AuthProtocol authPasswordEncryption = getAuthPasswordEncryption(snmpProtocol.getAuthPasswordEncryption()); + target = targetBuilder + .user(snmpProtocol.getUsername()) + .auth(authPasswordEncryption).authPassphrase(snmpProtocol.getAuthPassphrase()) + .priv(privatePasswordEncryption).privPassphrase(snmpProtocol.getPrivPassphrase()) + .done() + .timeout(timeout).retries(1) + .build(); + USM usm = new USM(SecurityProtocols.getInstance(), new OctetString(MPv3.createLocalEngineID()), 0); + SecurityModels.getInstance().addSecurityModel(usm); + snmpService.getUSM().addUser( + new OctetString(snmpProtocol.getUsername()), + new UsmUser(new OctetString(snmpProtocol.getUsername()), + AuthMD5.ID, + new OctetString(snmpProtocol.getAuthPassphrase()), + PrivDES.ID, + new OctetString(snmpProtocol.getPrivPassphrase())) + ); + } else if (snmpVersion == SnmpConstants.version1) { + target = targetBuilder + .v1() + .community(new OctetString(snmpProtocol.getCommunity())) + .timeout(timeout).retries(1) + .build(); + target.setSecurityModel(SecurityModel.SECURITY_MODEL_SNMPv1); + } else { + target = targetBuilder + .v2c() + .community(new OctetString(snmpProtocol.getCommunity())) + .timeout(timeout).retries(1) + .build(); + target.setSecurityModel(SecurityModel.SECURITY_MODEL_SNMPv2c); + } + String operation = snmpProtocol.getOperation(); + operation = StringUtils.hasText(operation) ? operation : OPERATION_GET; + if (OPERATION_GET.equalsIgnoreCase(operation)) { + String contextName = getContextName(snmpProtocol.getContextName()); + PDU pdu = targetBuilder.pdu().type(PDU.GET).oids(snmpProtocol.getOids().values().toArray(new String[0])).contextName(contextName).build(); + SnmpCompletableFuture snmpRequestFuture = SnmpCompletableFuture.send(snmpService, target, pdu); + List<VariableBinding> vbs = snmpRequestFuture.get().getAll(); + long responseTime = System.currentTimeMillis() - startTime; + Map<String, String> oidsMap = snmpProtocol.getOids(); + Map<String, String> oidsValueMap = new HashMap<>(oidsMap.size()); + for (VariableBinding binding : vbs) { + if (binding == null) { + continue; + } + Variable variable = binding.getVariable(); + if (variable instanceof TimeTicks timeTicks) { + String value = timeTicks.toString(FORMAT_PATTERN); + oidsValueMap.put(binding.getOid().toDottedString(), value); + } else { + oidsValueMap.put(binding.getOid().toDottedString(), binding.toValueString()); + } + } + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : metrics.getAliasFields()) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Long.toString(responseTime)); + } else { + String oid = oidsMap.get(alias); + String value = oidsValueMap.get(oid); + valueRowBuilder.addColumn(Objects.requireNonNullElse(value, CommonConstants.NULL_VALUE)); + } + } + builder.addValueRow(valueRowBuilder.build()); + } else if (OPERATION_WALK.equalsIgnoreCase(operation)) { + Map<String, String> oidMap = snmpProtocol.getOids(); + Assert.notEmpty(oidMap, "snmp oids is required when operation is walk."); + TableUtils tableUtils = new TableUtils(snmpService, new DefaultPDUFactory(PDU.GETBULK)); + OID[] oids = oidMap.values().stream().map(OID::new).toArray(OID[]::new); + List<TableEvent> tableEvents = tableUtils.getTable(target, oids, null, null); + Assert.notNull(tableEvents, "snmp walk response empty content."); + long responseTime = System.currentTimeMillis() - startTime; + for (TableEvent tableEvent : tableEvents) { + if (tableEvent == null || tableEvent.isError()) { + continue; + } + VariableBinding[] varBindings = tableEvent.getColumns(); + Map<String, String> oidsValueMap = new HashMap<>(varBindings.length); + for (VariableBinding binding : varBindings) { + if (binding == null) { + continue; + } + Variable variable = binding.getVariable(); + if (variable instanceof TimeTicks timeTicks) { + String value = timeTicks.toString(FORMAT_PATTERN); + oidsValueMap.put(binding.getOid().trim().toDottedString(), value); + } else { + oidsValueMap.put(binding.getOid().trim().toDottedString(), bingdingHexValueToString(binding)); + } + } + // when too many empty value field, ignore + if (oidsValueMap.size() < metrics.getAliasFields().size() / 2) { + continue; + } + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : metrics.getAliasFields()) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Long.toString(responseTime)); + } else { + String oid = oidMap.get(alias); + String value = oidsValueMap.get(oid); + if (value == null) { + // get leaf + for (String key : oidsValueMap.keySet()) { + if (key.startsWith(oid)) { + value = oidsValueMap.get(key); + break; + } + } + } + valueRowBuilder.addColumn(Objects.requireNonNullElse(value, CommonConstants.NULL_VALUE)); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + } catch (ExecutionException | InterruptedException ex) { + String errorMsg = CommonUtil.getMessageFromThrowable(ex); + log.warn("[snmp collect] error: {}", errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn("[snmp collect] error: {}", errorMsg, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (snmpService != null) { + if (snmpVersion == SnmpConstants.version3) { + try { + snmpClose(snmpService, SnmpConstants.version3); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_SNMP; + } + + private synchronized Snmp getSnmpService(int snmpVersion, SnmpBuilder snmpBuilder) throws IOException { + Snmp snmpService = versionSnmpService.get(snmpVersion); + if (snmpService != null) { + return snmpService; + } + if (snmpVersion == SnmpConstants.version3) { + snmpService = snmpBuilder.udp().v3().securityProtocols(SecurityProtocols.SecurityProtocolSet.maxCompatibility).usm().threads(4).build(); + } else if (snmpVersion == SnmpConstants.version1) { + snmpService = snmpBuilder.udp().v1().threads(4).build(); + } else { + snmpService = snmpBuilder.udp().v2c().threads(4).build(); + } + versionSnmpService.put(snmpVersion, snmpService); + return snmpService; + } + + private int getSnmpVersion(String snmpVersion) { + int version = SnmpConstants.version2c; + if (!StringUtils.hasText(snmpVersion)) { + return version; + } + if (snmpVersion.equalsIgnoreCase(String.valueOf(SnmpConstants.version1)) + || snmpVersion.equalsIgnoreCase(TargetBuilder.SnmpVersion.v1.name())) { + return SnmpConstants.version1; + } + if (snmpVersion.equalsIgnoreCase(String.valueOf(SnmpConstants.version3)) + || snmpVersion.equalsIgnoreCase(TargetBuilder.SnmpVersion.v3.name())) { + return SnmpConstants.version3; + } + return version; + } + + private String bingdingHexValueToString(VariableBinding binding) { + // whether if binding is hex + String hexString = binding.toValueString(); + if (hexString.contains(HEX_SPLIT)) { + try { + String clearHexStr = hexString.replace(HEX_SPLIT, ""); + byte[] bytes = HexFormat.of().parseHex(clearHexStr); + CharsetDecoder decoder = Charset.forName("GB2312").newDecoder(); + try { + CharBuffer res = decoder.decode(ByteBuffer.wrap(bytes)); + return res.toString(); + } catch (Exception e) { + if (isDateAndTimeOctetString(binding)) { + return parseDateAndTime(clearHexStr); + } + return new String(bytes); + } + } catch (Exception e) { + return hexString; + } + } else { + return hexString; + } + } + + private static boolean isDateAndTimeOctetString(VariableBinding binding) { + if (!(binding.getVariable() instanceof OctetString)) { + return false; + } + byte[] bytes = HexFormat.of().parseHex(binding.toValueString().replaceAll(HEX_SPLIT, "")); + if (bytes.length != 8 && bytes.length != 11) return false; + + int year = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF); + if (year < 1970 || year > 3000) return false; + + int month = bytes[2] & 0xFF; + if (month < 1 || month > 12) return false; + + int day = bytes[3] & 0xFF; + if (day < 1 || day > 31) return false; + + int hour = bytes[4] & 0xFF; + if (hour > 23) return false; + + int minute = bytes[5] & 0xFF; + if (minute > 59) return false; + + int second = bytes[6] & 0xFF; + if (second > 59) return false; + + int deciSecond = bytes[7] & 0xFF; + if (deciSecond > 99) return false; + + if (bytes.length == 11) { + int tzSign = bytes[8] & 0xFF; + if (tzSign != 0x2B && tzSign != 0x2D) return false; + + int tzHour = bytes[9] & 0xFF; + if (tzHour > 23) return false; + + int tzMinute = bytes[10] & 0xFF; + if (tzMinute > 59) return false; + } + return true; + } + + private static String parseDateAndTime(String hexString) { + byte[] bytes = HexFormat.of().parseHex(hexString); + int year = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF); + int month = bytes[2] & 0xFF; + int day = bytes[3] & 0xFF; + int hour = bytes[4] & 0xFF; + int minute = bytes[5] & 0xFF; + int second = bytes[6] & 0xFF; + int deciSeconds = bytes[7] & 0xFF; + String dateTime = String.format(DATE_AND_TIME_PATTERN, + year, month, day, hour, minute, second, deciSeconds); + if (bytes.length == 11) { + char sign = (char) bytes[8]; + int tzHour = bytes[9] & 0xFF; + int tzMinute = bytes[10] & 0xFF; + dateTime += String.format(" " + TIME_ZONE_PATTERN, sign, tzHour, tzMinute); + } + return dateTime; + } + + + private void snmpClose(Snmp snmp, int version) throws IOException { + snmp.close(); + versionSnmpService.remove(version); + } + + private TargetBuilder.PrivProtocol getPrivPasswordEncryption(String privPasswordEncryption) { + if (privPasswordEncryption == null) { + return TargetBuilder.PrivProtocol.des; + } else if (AES128.equals(privPasswordEncryption)) { + return TargetBuilder.PrivProtocol.aes128; + } else { + return TargetBuilder.PrivProtocol.des; + } + } + + private TargetBuilder.AuthProtocol getAuthPasswordEncryption(String authPasswordEncryption) { + if (authPasswordEncryption == null) { + return TargetBuilder.AuthProtocol.md5; + } else if (SHA1.equals(authPasswordEncryption)) { + return TargetBuilder.AuthProtocol.sha1; + } else { + return TargetBuilder.AuthProtocol.md5; + } + } + + private String getContextName(String contextName) { + return contextName == null ? "" : contextName; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ssh/SshCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ssh/SshCollectImpl.java new file mode 100644 index 0000000..41b2987 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ssh/SshCollectImpl.java @@ -0,0 +1,306 @@ +/* + * 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.collector.collect.ssh; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.ssh.CommonSshBlacklist; +import org.apache.hertzbeat.collector.collect.common.ssh.SshHelper; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.SshProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.sshd.client.channel.ClientChannel; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.channel.exception.SshChannelOpenException; +import org.apache.sshd.common.future.CloseFuture; +import org.apache.sshd.common.util.io.output.NoCloseOutputStream; +import org.springframework.util.StringUtils; + +/** + * Ssh protocol collection implementation + */ +@Slf4j +public class SshCollectImpl extends AbstractCollect { + + private static final String PARSE_TYPE_ONE_ROW = "oneRow"; + private static final String PARSE_TYPE_MULTI_ROW = "multiRow"; + private static final String PARSE_TYPE_NETCAT = "netcat"; + private static final String PARSE_TYPE_LOG = "log"; + + private static final int DEFAULT_TIMEOUT = 10_000; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getSsh() == null) { + throw new IllegalArgumentException("ssh collect must has ssh params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + + long startTime = System.currentTimeMillis(); + SshProtocol sshProtocol = metrics.getSsh(); + boolean reuseConnection = Boolean.parseBoolean(sshProtocol.getReuseConnection()); + boolean useProxy = Boolean.parseBoolean(sshProtocol.getUseProxy()) + && StringUtils.hasText(sshProtocol.getProxyHost()); + int timeout = CollectUtil.getTimeout(sshProtocol.getTimeout(), DEFAULT_TIMEOUT); + ClientChannel channel = null; + ClientSession clientSession = null; + try { + clientSession = getConnectSession(sshProtocol, timeout, reuseConnection, useProxy); + if (CommonSshBlacklist.isCommandBlacklisted(sshProtocol.getScript())) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("The command is blacklisted: " + sshProtocol.getScript()); + log.warn("The command is blacklisted: {}", sshProtocol.getScript()); + return; + } + channel = clientSession.createExecChannel(sshProtocol.getScript()); + ByteArrayOutputStream response = new ByteArrayOutputStream(); + channel.setOut(response); + channel.setErr(new NoCloseOutputStream(System.err)); + channel.open().verify(timeout); + List<ClientChannelEvent> list = new ArrayList<>(); + list.add(ClientChannelEvent.CLOSED); + Collection<ClientChannelEvent> waitEvents = channel.waitFor(list, timeout); + if (waitEvents.contains(ClientChannelEvent.TIMEOUT)) { + // A cancel signal needs to be sent if the execution times out, otherwise the session cannot be closed promptly + int cancelSignal = 3; + channel.getInvertedIn().write(cancelSignal); + channel.getInvertedIn().flush(); + throw new SocketTimeoutException("Failed to retrieve command result in time: " + sshProtocol.getScript()); + } + Long responseTime = System.currentTimeMillis() - startTime; + String result = response.toString(); + if (!StringUtils.hasText(result)) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("ssh shell response data is null"); + return; + } + switch (sshProtocol.getParseType()) { + case PARSE_TYPE_LOG -> parseResponseDataByLog(result, metrics.getAliasFields(), builder, responseTime); + case PARSE_TYPE_NETCAT -> parseResponseDataByNetcat(result, metrics.getAliasFields(), builder, responseTime); + case PARSE_TYPE_ONE_ROW -> parseResponseDataByOne(result, metrics.getAliasFields(), builder, responseTime); + case PARSE_TYPE_MULTI_ROW -> parseResponseDataByMulti(result, metrics.getAliasFields(), builder, responseTime); + default -> { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Ssh collect not support this parse type: " + sshProtocol.getParseType()); + } + } + } catch (ConnectException connectException) { + String errorMsg = CommonUtil.getMessageFromThrowable(connectException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("The peer refused to connect: service port does not listening or firewall: " + errorMsg); + } catch (SshException sshException) { + Throwable throwable = sshException.getCause(); + if (throwable instanceof SshChannelOpenException) { + log.warn("Remote ssh server no more session channel, please increase sshd_config MaxSessions."); + } + String errorMsg = CommonUtil.getMessageFromThrowable(sshException); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer ssh connection failed: " + errorMsg); + } catch (IOException ioException) { + String errorMsg = CommonUtil.getMessageFromThrowable(ioException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer io connection failed: " + errorMsg); + } catch (Exception exception) { + String errorMsg = CommonUtil.getMessageFromThrowable(exception); + log.warn(errorMsg, exception); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + boolean channelClosed = true; + try { + channelClosed = closeChannel(channel, timeout); + } catch (Exception e) { + channelClosed = false; + log.error("Failed to close SSH channel", e); + } + if (clientSession != null && (!channelClosed || (!reuseConnection && !useProxy))) { + try { + clientSession.close(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + } + } + + static boolean closeChannel(ClientChannel channel, int timeout) throws IOException { + if (channel == null || channel.isClosed()) { + return true; + } + long startTime = System.currentTimeMillis(); + try { + CloseFuture closeFuture = channel.close(false); + if (!closeFuture.await(timeout)) { + log.warn("SSH channel graceful close timed out after {} ms, forcing local cleanup", timeout); + closeFuture = channel.close(true); + if (!closeFuture.await(timeout)) { + log.warn("SSH channel immediate close timed out after {} ms", timeout); + return false; + } + } + } catch (InterruptedIOException e) { + try { + channel.close(true); + } catch (RuntimeException closeException) { + e.addSuppressed(closeException); + } finally { + Thread.currentThread().interrupt(); + } + throw e; + } + log.debug("SSH channel closed in {} ms", System.currentTimeMillis() - startTime); + return true; + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_SSH; + } + + private void parseResponseDataByLog(String result, List<String> aliasFields, CollectRep.MetricsData.Builder builder, Long responseTime) { + String[] lines = result.split("\n"); + if (lines.length + 1 < aliasFields.size()) { + log.error("ssh response data not enough: {}", result); + return; + } + for (String line : lines) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(responseTime.toString()); + } else { + valueRowBuilder.addColumn(line); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + + private void parseResponseDataByNetcat(String result, List<String> aliasFields, CollectRep.MetricsData.Builder builder, Long responseTime) { + String[] lines = result.split("\n"); + if (lines.length + 1 < aliasFields.size()) { + log.error("ssh response data not enough: {}", result); + return; + } + boolean contains = lines[0].contains("="); + Map<String, String> mapValue = Arrays.stream(lines) + .map(item -> { + if (contains) { + return item.split("="); + } else { + return item.split("\t"); + } + }) + .filter(item -> item.length == 2) + .collect(Collectors.toMap(x -> x[0], x -> x[1])); + + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String field : aliasFields) { + String fieldValue = mapValue.get(field); + valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } + builder.addValueRow(valueRowBuilder.build()); + } + + private void parseResponseDataByOne(String result, List<String> aliasFields, CollectRep.MetricsData.Builder builder, Long responseTime) { + String[] lines = result.split("\n"); + if (lines.length + 1 < aliasFields.size()) { + log.error("ssh response data not enough: {}", result); + return; + } + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + int aliasIndex = 0; + int lineIndex = 0; + while (aliasIndex < aliasFields.size()) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(aliasFields.get(aliasIndex))) { + valueRowBuilder.addColumn(responseTime.toString()); + } else { + if (lineIndex < lines.length) { + valueRowBuilder.addColumn(lines[lineIndex].trim()); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + lineIndex++; + } + aliasIndex++; + } + builder.addValueRow(valueRowBuilder.build()); + } + + private void parseResponseDataByMulti(String result, List<String> aliasFields, + CollectRep.MetricsData.Builder builder, Long responseTime) { + String[] lines = result.split("\n"); + if (lines.length <= 1) { + log.error("ssh response data only has header: {}", result); + return; + } + String[] fields = lines[0].split(" "); + Map<String, Integer> fieldMapping = new HashMap<>(fields.length); + for (int i = 0; i < fields.length; i++) { + fieldMapping.put(fields[i].trim().toLowerCase(), i); + } + for (int i = 1; i < lines.length; i++) { + String[] values = lines[i].split(" "); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(responseTime.toString()); + } else { + Integer index = fieldMapping.get(alias.toLowerCase()); + if (index != null && index < values.length) { + valueRowBuilder.addColumn(values[index]); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + + private ClientSession getConnectSession(SshProtocol sshProtocol, int timeout, boolean reuseConnection, boolean useProxy) + throws IOException, GeneralSecurityException { + return SshHelper.getConnectSession(sshProtocol, timeout, reuseConnection, useProxy); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImpl.java new file mode 100644 index 0000000..20602ca --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImpl.java @@ -0,0 +1,147 @@ +/* + * 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.collector.collect.telnet; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.ConnectException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.net.telnet.TelnetClient; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.TelnetProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * telnet collect + */ +@Slf4j +public class TelnetCollectImpl extends AbstractCollect { + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getTelnet() == null) { + throw new IllegalArgumentException("Telnet collect must has Telnet params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + TelnetProtocol telnet = metrics.getTelnet(); + int timeout = CollectUtil.getTimeout(telnet.getTimeout()); + TelnetClient telnetClient = null; + try { + telnetClient = new TelnetClient("vt200"); + telnetClient.setConnectTimeout(timeout); + telnetClient.connect(telnet.getHost(), Integer.parseInt(telnet.getPort())); + if (telnetClient.isConnected()) { + long responseTime = System.currentTimeMillis() - startTime; + List<String> aliasFields = metrics.getAliasFields(); + String app = builder.getApp(); + Map<String, String> resultMap = execCmdAndParseResult(telnetClient, telnet.getCmd(), app); + resultMap.put(CollectorConstants.RESPONSE_TIME, Long.toString(responseTime)); + if (resultMap.size() < aliasFields.size()) { + log.error("telnet response data not enough: {}", resultMap); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("The cmd execution results do not match the expected number of metrics."); + return; + } + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String field : aliasFields) { + String fieldValue = resultMap.get(field); + valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } + builder.addValueRow(valueRowBuilder.build()); + } else { + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer connect failed,Timeout " + timeout + "ms"); + return; + } + telnetClient.disconnect(); + } catch (ConnectException connectException) { + String errorMsg = CommonUtil.getMessageFromThrowable(connectException); + log.debug(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("The peer refused to connect: service port does not listening or firewall: " + errorMsg); + } catch (IOException ioException) { + String errorMsg = CommonUtil.getMessageFromThrowable(ioException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer connect failed: " + errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn(errorMsg, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (telnetClient != null) { + try { + telnetClient.disconnect(); + } catch (Exception e) { + log.warn(e.getMessage()); + } + } + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_TELNET; + } + + private static Map<String, String> execCmdAndParseResult(TelnetClient telnetClient, String cmd, String app) throws IOException { + if (cmd == null || StringUtils.isEmpty(cmd.trim())) { + return new HashMap<>(16); + } + OutputStream outputStream = telnetClient.getOutputStream(); + outputStream.write(cmd.getBytes(StandardCharsets.UTF_8)); + outputStream.flush(); + String result = new String(telnetClient.getInputStream().readAllBytes()); + String[] lines = result.split("\n"); + if (CollectorConstants.ZOOKEEPER_APP.equals(app) && CollectorConstants.ZOOKEEPER_ENVI_HEAD.equals(lines[0])) { + lines = Arrays.stream(lines) + .skip(1) + .toArray(String[]::new); + } + boolean contains = lines[0].contains("="); + return Arrays.stream(lines) + .map(item -> { + if (contains) { + return item.split("="); + } else { + return item.split("\t"); + } + }) + .filter(item -> item.length == 2) + .collect(Collectors.toMap(x -> x[0], x -> x[1])); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/udp/UdpCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/udp/UdpCollectImpl.java new file mode 100644 index 0000000..dda766f --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/udp/UdpCollectImpl.java @@ -0,0 +1,101 @@ +/* + * 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.collector.collect.udp; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.net.PortUnreachableException; +import java.net.SocketAddress; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.UdpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * udp collect + */ +@Slf4j +public class UdpCollectImpl extends AbstractCollect { + + private static final byte[] HELLO = "hello".getBytes(StandardCharsets.UTF_8); + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getUdp() == null) { + throw new IllegalArgumentException("Udp collect must has udp params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + UdpProtocol udpProtocol = metrics.getUdp(); + int timeout = CollectUtil.getTimeout(udpProtocol.getTimeout()); + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(timeout); + String content = udpProtocol.getContent(); + byte[] buffer = CollectUtil.fromHexString(content); + buffer = buffer == null ? HELLO : buffer; + SocketAddress socketAddress = new InetSocketAddress(udpProtocol.getHost(), Integer.parseInt(udpProtocol.getPort())); + DatagramPacket request = new DatagramPacket(buffer, buffer.length, socketAddress); + socket.send(request); + byte[] responseBuffer = new byte[1]; + DatagramPacket response = new DatagramPacket(responseBuffer, responseBuffer.length); + socket.receive(response); + long responseTime = System.currentTimeMillis() - startTime; + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : metrics.getAliasFields()) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumn(Long.toString(responseTime)); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + builder.addValueRow(valueRowBuilder.build()); + } catch (SocketTimeoutException timeoutException) { + String errorMsg = CommonUtil.getMessageFromThrowable(timeoutException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer connect failed: " + errorMsg); + } catch (PortUnreachableException portUnreachableException) { + String errorMsg = CommonUtil.getMessageFromThrowable(portUnreachableException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_REACHABLE); + builder.setMsg("Peer port unreachable"); + } catch (Exception exception) { + String errorMsg = CommonUtil.getMessageFromThrowable(exception); + log.warn(errorMsg, exception); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_UDP; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/websocket/WebsocketCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/websocket/WebsocketCollectImpl.java new file mode 100644 index 0000000..1ff8c1a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/websocket/WebsocketCollectImpl.java @@ -0,0 +1,194 @@ +/* + * 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.collector.collect.websocket; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.WebsocketProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.springframework.util.Assert; + +/** + * Websocket Collect + */ +@Slf4j +public class WebsocketCollectImpl extends AbstractCollect { + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getWebsocket() == null) { + throw new IllegalArgumentException("Websocket collect must has Websocket params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + + WebsocketProtocol websocketProtocol = metrics.getWebsocket(); + // Compatible with monitoring templates without path parameters + if (StringUtils.isBlank(websocketProtocol.getPath())) { + websocketProtocol.setPath("/"); + } + checkParam(websocketProtocol); + String host = websocketProtocol.getHost(); + String port = websocketProtocol.getPort(); + Socket socket = null; + try { + socket = new Socket(); + SocketAddress socketAddress = new InetSocketAddress(host, Integer.parseInt(port)); + socket.connect(socketAddress); + + if (socket.isConnected()) { + long responseTime = System.currentTimeMillis() - startTime; + OutputStream out = socket.getOutputStream(); + InputStream in = socket.getInputStream(); + + send(out, websocketProtocol); + Map<String, String> resultMap = readHeaders(in); + resultMap.put(CollectorConstants.RESPONSE_TIME, Long.toString(responseTime)); + + // Close the output stream and socket connection + in.close(); + out.close(); + socket.close(); + List<String> aliasFields = metrics.getAliasFields(); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String field : aliasFields) { + String fieldValue = resultMap.get(field); + valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } + builder.addValueRow(valueRowBuilder.build()); + } else { + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Peer connect failed:"); + } + } catch (UnknownHostException unknownHostException) { + String errorMsg = CommonUtil.getMessageFromThrowable(unknownHostException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("UnknownHost:" + errorMsg); + } catch (SocketTimeoutException socketTimeoutException) { + String errorMsg = CommonUtil.getMessageFromThrowable(socketTimeoutException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Socket connect timeout: " + errorMsg); + } catch (IOException ioException) { + String errorMsg = CommonUtil.getMessageFromThrowable(ioException); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg("Connect may fail:" + errorMsg); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_WEBSOCKET; + } + + private void send(OutputStream out, WebsocketProtocol websocketProtocol) throws IOException { + byte[] key = generateRandomKey(); + String base64Key = base64Encode(key); + String requestLine = "GET " + websocketProtocol.getPath() + " HTTP/1.1\r\n"; + out.write(requestLine.getBytes(StandardCharsets.UTF_8)); + String hostName = InetAddress.getLocalHost().getHostAddress(); + out.write(("Host:" + hostName + "\r\n").getBytes(StandardCharsets.UTF_8)); + out.write("Upgrade: websocket\r\n".getBytes(StandardCharsets.UTF_8)); + out.write("Connection: Upgrade\r\n".getBytes(StandardCharsets.UTF_8)); + out.write("Sec-WebSocket-Version: 13\r\n".getBytes(StandardCharsets.UTF_8)); + out.write("Sec-WebSocket-Extensions: chat, superchat\r\n".getBytes(StandardCharsets.UTF_8)); + out.write(("Sec-WebSocket-Key: " + base64Key + "\r\n").getBytes(StandardCharsets.UTF_8)); + out.write("Content-Length: 0\r\n".getBytes(StandardCharsets.UTF_8)); + out.write("\r\n".getBytes(StandardCharsets.UTF_8)); + out.flush(); + } + + // Read response headers + private Map<String, String> readHeaders(InputStream in) throws IOException { + + Map<String, String> map = new HashMap<>(8); + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + + String line; + while ((line = reader.readLine()) != null && !line.isEmpty()) { + int separatorIndex = line.indexOf(':'); + if (separatorIndex != -1) { + String key = line.substring(0, separatorIndex).trim(); + String value = line.substring(separatorIndex + 1).trim(); + // Lowercase first letter + map.put(StringUtils.uncapitalize(key), value); + } else { + // Cut HTTP/1.1, 101, Switching Protocols + String[] parts = line.split("\\s+", 3); + if (parts.length == 3) { + for (String part : parts) { + if (part.startsWith("HTTP")) { + map.put("httpVersion", part); + } else if (StringUtils.isNotBlank(part) && Character.isDigit(part.charAt(0))) { + map.put("responseCode", part); + } else { + map.put("statusMessage", part); + } + } + } + } + } + return map; + } + + private byte[] generateRandomKey() { + SecureRandom secureRandom = new SecureRandom(); + byte[] key = new byte[16]; + secureRandom.nextBytes(key); + return key; + } + + private void checkParam(WebsocketProtocol protocol) { + Assert.hasText(protocol.getHost(), "Websocket Protocol host is required."); + Assert.hasText(protocol.getPort(), "Websocket Protocol port is required."); + Assert.hasText(protocol.getPath(), "Websocket Protocol path is required."); + } + + private String base64Encode(byte[] data) { + return Base64.getEncoder().encodeToString(data); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/common/http/CommonHttpClientTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/common/http/CommonHttpClientTest.java new file mode 100644 index 0000000..6cd9f3e --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/common/http/CommonHttpClientTest.java @@ -0,0 +1,44 @@ +/* + * 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.collector.collect.common.http; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link CommonHttpClient} + */ +class CommonHttpClientTest { + + @BeforeEach + void setUp() { + } + + @AfterEach + void tearDown() { + } + + @Test + void getHttpClient() { + } + + @Test + void getConnectionManager() { + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/database/JdbcCommonCollectTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/database/JdbcCommonCollectTest.java new file mode 100644 index 0000000..3c10a31 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/database/JdbcCommonCollectTest.java @@ -0,0 +1,357 @@ +/* + * 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.collector.collect.database; + +import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier; +import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test case for {@link JdbcCommonCollect} + */ +class JdbcCommonCollectTest { + private JdbcCommonCollect jdbcCommonCollect; + + @BeforeEach + void setup() { + jdbcCommonCollect = new JdbcCommonCollect(); + } + + @Test + void preCheck() { + assertThrows(IllegalArgumentException.class, () -> { + jdbcCommonCollect.preCheck(null); + }); + assertThrows(IllegalArgumentException.class, () -> { + Metrics metrics = new Metrics(); + jdbcCommonCollect.preCheck(metrics); + }); + + assertDoesNotThrow(() -> { + JdbcProtocol jdbc = new JdbcProtocol(); + jdbc.setUrl("jdbc:mysql://localhost:3306/test"); + + Metrics metrics = new Metrics(); + metrics.setJdbc(jdbc); + jdbcCommonCollect.preCheck(metrics); + }); + + String[] invalidKeywords = new String[]{ + "allowLoadLocalInfile", "allowLoadLocalInfileInPath", "useLocalInfile" + }; + for (String keyword : invalidKeywords) { + // contains not allowed keywords + assertThrows(IllegalArgumentException.class, () -> { + JdbcProtocol jdbc = new JdbcProtocol(); + jdbc.setUrl("jdbc:mysql://localhost:3306/test?" + keyword); + + Metrics metrics = new Metrics(); + metrics.setJdbc(jdbc); + jdbcCommonCollect.preCheck(metrics); + }); + } + } + + @Test + void collect() { + assertDoesNotThrow(() -> { + JdbcProtocol jdbc = new JdbcProtocol(); + jdbc.setUrl("jdbc:mysql://localhost:3306/test"); + jdbc.setUsername("root"); + jdbc.setPassword("123456"); + jdbc.setQueryType("select"); + + Metrics metrics = new Metrics(); + metrics.setJdbc(jdbc); + + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + jdbcCommonCollect.collect(builder, metrics); + }); + } + + @Test + void supportProtocol() { + String protocol = jdbcCommonCollect.supportProtocol(); + assertEquals(DispatchConstants.PROTOCOL_JDBC, protocol); + } + + @Test + void testUrlPassThrough() { + String[] testUrls = { + "jdbc:mysql://localhost:3306/test?allowPublicKeyRetrieval=true&useSSL=false", + "jdbc:mysql://localhost:3306/test?usessl=false&verifyServerCertificate=true", + "jdbc:mysql://localhost:3306/test?serverTimezone=UTC&autoReconnect=false" + }; + JdbcCommonCollect jdbcCollect = new JdbcCommonCollect(); + for (String originalUrl : testUrls) { + try { + JdbcProtocol jdbcProtocol = JdbcProtocol.builder() + .host("localhost") + .port("3306") + .platform("mysql") + .username("root") + .password("root") + .database("test") + .url(originalUrl) + .build(); + + String processedUrl = constructDatabaseUrl(jdbcCollect, jdbcProtocol, "localhost", "3306"); + // Verify that the processed URL is the same as the original URL + assertEquals(originalUrl, processedUrl, + "URL should be passed through without modification: " + originalUrl); + } catch (Exception e) { + System.out.println("URL rejected by security validation: " + originalUrl + ", reason: " + e.getMessage()); + } + } + } + + @Test + void testConstructDatabaseUrlByPlatform() throws Exception { + Map<String, String> expectedUrls = new LinkedHashMap<>(); + expectedUrls.put("mysql", "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false"); + expectedUrls.put("mariadb", "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false"); + expectedUrls.put("postgresql", "jdbc:postgresql://localhost:3306/test"); + expectedUrls.put("clickhouse", "jdbc:clickhouse://localhost:3306/test"); + expectedUrls.put("sqlserver", "jdbc:sqlserver://localhost:3306;DatabaseName=test;trustServerCertificate=true;"); + expectedUrls.put("oracle", "jdbc:oracle:thin:@localhost:3306/test"); + expectedUrls.put("dm", "jdbc:dm://localhost:3306"); + + for (Map.Entry<String, String> entry : expectedUrls.entrySet()) { + JdbcProtocol jdbcProtocol = JdbcProtocol.builder() + .platform(entry.getKey()) + .database("test") + .build(); + + assertEquals(entry.getValue(), constructDatabaseUrl(jdbcCommonCollect, jdbcProtocol, "localhost", "3306")); + } + } + + @Test + void testConstructDatabaseUrlRejectsUnsupportedPlatform() { + JdbcProtocol jdbcProtocol = JdbcProtocol.builder() + .platform("invalid") + .database("test") + .build(); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> constructDatabaseUrl(jdbcCommonCollect, jdbcProtocol, "localhost", "3306")); + assertEquals("Not support database platform: invalid", exception.getMessage()); + } + + @Test + void testCloseConnectionWhenCreateStatementFails() throws Exception { + String url = "jdbc:postgresql://localhost:5432/hertzbeat"; + String username = "root"; + String password = "root"; + Connection connection = mock(Connection.class); + when(connection.createStatement()).thenThrow(new SQLException("create statement failed")); + + try (MockedStatic<DriverManager> driverManager = mockStatic(DriverManager.class)) { + driverManager.when(() -> DriverManager.getConnection(url, username, password)).thenReturn(connection); + + SQLException exception = assertThrows(SQLException.class, + () -> getConnection(jdbcCommonCollect, username, password, url, 1000, false)); + + assertEquals("create statement failed", exception.getMessage()); + verify(connection).close(); + } + } + + @Test + void testReuseConnectionSerializesCreationForSameIdentifier() throws Exception { + String url = "jdbc:hertzbeat-test:concurrent"; + String username = "root"; + String password = "root"; + Connection firstConnection = mock(Connection.class); + Connection secondConnection = mock(Connection.class); + Statement firstStatement = mock(Statement.class); + Statement reusedStatement = mock(Statement.class); + when(firstConnection.createStatement()).thenReturn(firstStatement, reusedStatement); + when(secondConnection.createStatement()).thenReturn(mock(Statement.class)); + + BlockingJdbcCommonCollect collect = new BlockingJdbcCommonCollect(firstConnection, secondConnection); + CacheIdentifier identifier = CacheIdentifier.builder() + .ip(url).username(username).password(password).build(); + GlobalConnectionCache.getInstance().removeCache(identifier); + + try (ExecutorService executor = Executors.newFixedThreadPool(2)) { + Future<Statement> first = executor.submit(() -> getConnection(collect, username, password, url, 1000, true)); + assertTrue(collect.firstOpenStarted.await(5, TimeUnit.SECONDS)); + + CountDownLatch secondTaskStarted = new CountDownLatch(1); + Future<Statement> second = executor.submit(() -> { + secondTaskStarted.countDown(); + return getConnection(collect, username, password, url, 1000, true); + }); + assertTrue(secondTaskStarted.await(5, TimeUnit.SECONDS)); + boolean secondPhysicalConnectionOpened = collect.secondOpenStarted.await(1, TimeUnit.SECONDS); + collect.allowFirstOpen.countDown(); + + assertSame(firstStatement, first.get(5, TimeUnit.SECONDS)); + assertSame(reusedStatement, second.get(5, TimeUnit.SECONDS)); + assertFalse(secondPhysicalConnectionOpened); + assertEquals(1, collect.openCount.get()); + } finally { + collect.allowFirstOpen.countDown(); + GlobalConnectionCache.getInstance().removeCache(identifier); + } + } + + @Test + void testConstructDatabaseUrlSecurityInterception() { + JdbcCommonCollect jdbcCollect = new JdbcCommonCollect(); + String[] maliciousUrls = { + // URL length limit test + "jdbc:mysql://localhost:3306/test?" + "a".repeat(2050) + "=value", + // url format check + "jdbca:mysql://localhost:3306/test?allowLoadLocalInfile=true", + // backlist check + "jdbc:mysql://localhost:3306/test?allowLoadLocalInfile=true", + // universal detection of JDBC injection and deserialization attacks + "jdbc:mysql://localhost:3306/test?jndi:ldap://duansg.com/exploit", + // universal detection of bypass + "jdbc:mysql://localhost:3306/test?param=create\\trigger", + // database platform specific bypass detection + "jdbc:mysql://localhost:3306/test?allow\\nload\\nlocal\\ninfile=true" + + }; + + // Test malicious URLs - should throw exceptions + for (String maliciousUrl : maliciousUrls) { + JdbcProtocol jdbcProtocol = JdbcProtocol.builder() + .host("localhost") + .port("3306") + .platform("mysql") + .username("root") + .password("root") + .database("test") + .url(maliciousUrl) + .build(); + + assertThrows(Exception.class, () -> { + constructDatabaseUrl(jdbcCollect, jdbcProtocol, "localhost", "3306"); + }, "Malicious URL should be blocked: " + maliciousUrl); + } + } + + private String constructDatabaseUrl(JdbcCommonCollect jdbcCollect, JdbcProtocol jdbcProtocol, + String host, String port) throws Exception { + try { + Method constructMethod = JdbcCommonCollect.class + .getDeclaredMethod("constructDatabaseUrl", JdbcProtocol.class, String.class, String.class); + constructMethod.setAccessible(true); + return (String) constructMethod.invoke(jdbcCollect, jdbcProtocol, host, port); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception exception) { + throw exception; + } + if (cause instanceof Error error) { + throw error; + } + throw new RuntimeException(cause); + } + } + + private Statement getConnection(JdbcCommonCollect jdbcCollect, String username, String password, String url, + Integer timeout, boolean reuseConnection) throws Exception { + try { + Method getConnectionMethod = JdbcCommonCollect.class + .getDeclaredMethod("getConnection", String.class, String.class, String.class, + Integer.class, boolean.class); + getConnectionMethod.setAccessible(true); + return (Statement) getConnectionMethod.invoke(jdbcCollect, username, password, url, timeout, + reuseConnection); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception exception) { + throw exception; + } + if (cause instanceof Error error) { + throw error; + } + throw new RuntimeException(cause); + } + } + + private static final class BlockingJdbcCommonCollect extends JdbcCommonCollect { + + private final Connection firstConnection; + private final Connection secondConnection; + private final CountDownLatch firstOpenStarted = new CountDownLatch(1); + private final CountDownLatch secondOpenStarted = new CountDownLatch(1); + private final CountDownLatch allowFirstOpen = new CountDownLatch(1); + private final AtomicInteger openCount = new AtomicInteger(); + + private BlockingJdbcCommonCollect(Connection firstConnection, Connection secondConnection) { + this.firstConnection = firstConnection; + this.secondConnection = secondConnection; + } + + @Override + Connection openConnection(String url, String username, String password) throws SQLException { + if (openCount.incrementAndGet() == 1) { + firstOpenStarted.countDown(); + try { + if (!allowFirstOpen.await(5, TimeUnit.SECONDS)) { + throw new SQLException("Timed out waiting to open the first connection"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new SQLException("Interrupted while opening the first connection", e); + } + return firstConnection; + } + secondOpenStarted.countDown(); + return secondConnection; + } + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/database/JdbcSpiLoaderTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/database/JdbcSpiLoaderTest.java new file mode 100644 index 0000000..a3fa253 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/database/JdbcSpiLoaderTest.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.database; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link JdbcSpiLoader} + */ +class JdbcSpiLoaderTest { + + @BeforeEach + void setUp() { + } + + @Test + void run() { + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/dns/DnsCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/dns/DnsCollectImplTest.java new file mode 100644 index 0000000..cbdd485 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/dns/DnsCollectImplTest.java @@ -0,0 +1,104 @@ +/* + * 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.collector.collect.dns; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Collections; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.DnsProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link DnsCollectImpl} + */ +public class DnsCollectImplTest { + private DnsProtocol dnsProtocol; + private DnsCollectImpl dnsCollect; + + @BeforeEach + public void setup() { + dnsCollect = new DnsCollectImpl(); + dnsProtocol = DnsProtocol.builder() + .dnsServerIP("8.8.8.8") + .queryClass("IN") + .address("www.google.com") + .timeout("3000") + .recordType("A") + .port("53") + .tcp("tcp") + .build(); + } + + @Test + public void testPreCheck() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + //metrics is null + dnsCollect.collect(builder, null); + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + + //invalid DnsProtocol + Metrics metrics = new Metrics(); + assertThrows(IllegalArgumentException.class, () -> { + DnsProtocol dns = DnsProtocol.builder().build(); + metrics.setDns(dns); + dnsCollect.preCheck(metrics); + }); + + //validated DnsProtocol + assertDoesNotThrow(() -> { + metrics.setName("question"); + metrics.setDns(dnsProtocol); + dnsCollect.preCheck(metrics); + }); + } + + @Test + public void testCollect() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + Metrics metrics0 = Metrics.builder() + .name("question") + .dns(dnsProtocol) + .aliasFields(Collections.singletonList("section")) + .build(); + dnsCollect.collect(builder, metrics0); + assertEquals(CollectRep.Code.SUCCESS, builder.getCode()); + assertNotNull(builder.getValues(0).getColumns(0)); + + // metric name is header + Metrics metrics1 = Metrics.builder() + .name("header") + .dns(dnsProtocol) + .aliasFields(Collections.singletonList("section")) + .build(); + dnsCollect.collect(builder, metrics1); + assertEquals(CollectRep.Code.SUCCESS, builder.getCode()); + } + + @Test + public void testSupportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_DNS, dnsCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImplTest.java new file mode 100644 index 0000000..5af290d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImplTest.java @@ -0,0 +1,147 @@ +/* + * 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.collector.collect.ftp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.net.ftp.FTPClient; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.FtpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link FtpCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +class FtpCollectImplTest { + + @InjectMocks + private FtpCollectImpl ftpCollectImpl; + + @Test + void testPreCheck() { + FtpProtocol ftpProtocol = FtpProtocol.builder() + .host("127.0.0.1") + .username("admin") + .password("123456") + .port("21") + .direction("/") + .build(); + + List<String> aliasField = new ArrayList<>(); + aliasField.add("isActive"); + aliasField.add("responseTime"); + Metrics metrics = new Metrics(); + metrics.setName("server"); + metrics.setFtp(ftpProtocol); + metrics.setAliasFields(aliasField); + assertThrows(IllegalArgumentException.class, () -> ftpCollectImpl.preCheck(metrics)); + + } + + @Test + void testCollect() throws IOException { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + FtpProtocol ftpProtocol = FtpProtocol.builder() + .host("127.0.0.1") + .username("admin") + .password("123456") + .port("21") + .timeout("3000") + .direction("/") + .build(); + + boolean isActive = false; + MockedConstruction<FTPClient> mocked = Mockito.mockConstruction(FTPClient.class, + (ftpClient, context) -> { + Mockito.doNothing().when(ftpClient).connect(ftpProtocol.getHost(), + Integer.parseInt(ftpProtocol.getPort())); + + Mockito.doAnswer(invocationOnMock -> true).when(ftpClient) + .login(ftpProtocol.getUsername(), ftpProtocol.getPassword()); + Mockito.when(ftpClient.changeWorkingDirectory(ftpProtocol.getDirection())).thenReturn(isActive); + Mockito.doNothing().when(ftpClient).disconnect(); + }); + + + List<String> aliasField = new ArrayList<>(); + aliasField.add("isActive"); + aliasField.add("responseTime"); + Metrics metrics = new Metrics(); + metrics.setFtp(ftpProtocol); + metrics.setAliasFields(aliasField); + ftpCollectImpl.preCheck(metrics); + ftpCollectImpl.collect(builder, metrics); + assertEquals(builder.getValuesCount(), 1); + for (CollectRep.ValueRow valueRow : builder.getValuesList()) { + assertEquals(Boolean.toString(isActive), valueRow.getColumns(0)); + assertNotNull(valueRow.getColumns(1)); + } + mocked.close(); + + } + + @Test + void testAnonymousCollect() throws IOException { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + FtpProtocol ftpProtocol = FtpProtocol.builder() + .host("127.0.0.1") + .port("21") + .timeout("3000") + .direction("/") + .build(); + + boolean isActive = true; + MockedConstruction<FTPClient> mocked = Mockito.mockConstruction(FTPClient.class, + (ftpClient, context) -> { + Mockito.doNothing().when(ftpClient).connect(ftpProtocol.getHost(), + Integer.parseInt(ftpProtocol.getPort())); + Mockito.doAnswer(invocationOnMock -> true).when(ftpClient).login("anonymous", "password"); + Mockito.when(ftpClient.changeWorkingDirectory(ftpProtocol.getDirection())).thenReturn(isActive); + Mockito.doNothing().when(ftpClient).disconnect(); + }); + + List<String> aliasField = new ArrayList<>(); + aliasField.add("isActive"); + aliasField.add("responseTime"); + Metrics metrics = new Metrics(); + metrics.setFtp(ftpProtocol); + metrics.setAliasFields(aliasField); + + ftpCollectImpl.collect(builder, metrics); + assertEquals(builder.getValuesCount(), 1); + for (CollectRep.ValueRow valueRow : builder.getValuesList()) { + assertEquals(Boolean.toString(isActive), valueRow.getColumns(0)); + assertNotNull(valueRow.getColumns(1)); + } + mocked.close(); + + } + + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImplTest.java new file mode 100644 index 0000000..05199a2 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImplTest.java @@ -0,0 +1,634 @@ +/* + * 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.collector.collect.http; + +import com.google.common.collect.Lists; +import com.sun.net.httpserver.HttpServer; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test case for {@link HttpCollectImpl} + */ +class HttpCollectImplTest { + private HttpCollectImpl httpCollectImpl; + + @BeforeEach + void setUp() { + httpCollectImpl = new HttpCollectImpl(); + } + + @Test + void preCheck() { + assertThrows(IllegalArgumentException.class, () -> { + httpCollectImpl.preCheck(null); + }); + + assertThrows(IllegalArgumentException.class, () -> { + Metrics metrics = Metrics.builder().build(); + httpCollectImpl.preCheck(metrics); + }); + } + + @Test + void collect() { + HttpProtocol http = HttpProtocol.builder().build(); + http.setMethod("POST"); + Metrics metrics = Metrics.builder() + .http(http) + .build(); + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + + httpCollectImpl.collect(builder, metrics); + } + + @Test + void supportProtocol() { + String protocol = httpCollectImpl.supportProtocol(); + assert "http".equals(protocol); + } + + @Test + void parseResponseByWebsite() { + HttpProtocol http = HttpProtocol.builder().build(); + http.setMethod("GET"); + http.setHost("http://127.0.0.1"); + http.setUrl("/"); + http.setPort("8428"); + http.setParseType("website"); + http.setEnableUrlEncoding("true"); + Metrics metrics = Metrics.builder() + .http(http) + .aliasFields(Lists.newArrayList("responseTime", "keyword", "statusCode")) + .build(); + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + httpCollectImpl.collect(builder, metrics); + + assertNotNull(builder.getValuesList()); + for (CollectRep.ValueRow row : builder.getValuesList()) { + assertNotNull(row.getColumns(0)); + assertEquals(row.getColumns(1), "0"); + assertEquals(row.getColumns(2), "200"); + } + } + + @Test + void parseResponseByXmlPath() throws Exception { + // Create a sample XML response + String xmlResponse = """ + <?xml version="1.0" encoding="UTF-8"?> + <root> + <server> + <name>Server1</name> + <status>Running</status> + <metrics> + <cpu>75.5</cpu> + <memory>1024</memory> + <disk>500</disk> + </metrics> + </server> + <server> + <name>Server2</name> + <status>Stopped</status> + <metrics> + <cpu>0.0</cpu> + <memory>0</memory> + <disk>500</disk> + </metrics> + </server> + </root> + """; + + // Set up HttpProtocol with XML path parsing + HttpProtocol http = HttpProtocol.builder() + .parseType(DispatchConstants.PARSE_XML_PATH) + .parseScript("//server") // XPath to select all server nodes + .build(); + + // Set up Metrics with fields that have XPath expressions + List<Metrics.Field> fields = new ArrayList<>(); + fields.add(Metrics.Field.builder().field("name").build()); + fields.add(Metrics.Field.builder().field("status").build()); + fields.add(Metrics.Field.builder().field("metrics/cpu").build()); + fields.add(Metrics.Field.builder().field("metrics/memory").build()); + + Metrics metrics = Metrics.builder() + .http(http) + .fields(fields) + .aliasFields(Arrays.asList("name", "status", "metrics/cpu", "metrics/memory")) + .build(); + + // Create a custom builder that captures added rows + List<CollectRep.ValueRow> capturedRows = new ArrayList<>(); + CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() { + @Override + public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) { + capturedRows.add(valueRow); + return super.addValueRow(valueRow); + } + }; + + // Use reflection to access the private parseResponseByXmlPath method + Method parseMethod = HttpCollectImpl.class.getDeclaredMethod( + "parseResponseByXmlPath", + String.class, + Metrics.class, + CollectRep.MetricsData.Builder.class, + Long.class); + parseMethod.setAccessible(true); + + // Call the method + parseMethod.invoke(httpCollectImpl, xmlResponse, metrics, builder, 100L); + + // Verify the results + assertEquals(2, capturedRows.size(), "Should have parsed 2 server nodes"); + + // Check first server + CollectRep.ValueRow firstRow = capturedRows.get(0); + assertEquals(4, firstRow.getColumnsCount(), "First row should have 4 columns"); + assertEquals("Server1", firstRow.getColumns(0), "First server name should be Server1"); + assertEquals("Running", firstRow.getColumns(1), "First server status should be Running"); + assertEquals("75.5", firstRow.getColumns(2), "First server CPU should be 75.5"); + assertEquals("1024", firstRow.getColumns(3), "First server memory should be 1024"); + + // Check second server + CollectRep.ValueRow secondRow = capturedRows.get(1); + assertEquals(4, secondRow.getColumnsCount(), "Second row should have 4 columns"); + assertEquals("Server2", secondRow.getColumns(0), "Second server name should be Server2"); + assertEquals("Stopped", secondRow.getColumns(1), "Second server status should be Stopped"); + assertEquals("0.0", secondRow.getColumns(2), "Second server CPU should be 0.0"); + assertEquals("0", secondRow.getColumns(3), "Second server memory should be 0"); + } + + @Test + void parseResponseByXmlPathHonorsXmlEncodingDeclaration() throws Exception { + String xmlResponse = """ + <?xml version="1.0" encoding="ISO-8859-1"?> + <root> + <item> + <name>café</name> + </item> + </root> + """; + byte[] responseBytes = xmlResponse.getBytes(StandardCharsets.ISO_8859_1); + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/metrics", exchange -> { + exchange.getResponseHeaders().add("Content-Type", "application/xml"); + exchange.sendResponseHeaders(200, responseBytes.length); + exchange.getResponseBody().write(responseBytes); + exchange.close(); + }); + server.start(); + + try { + HttpProtocol http = HttpProtocol.builder() + .method("GET") + .host("127.0.0.1") + .port(String.valueOf(server.getAddress().getPort())) + .url("/metrics") + .parseType(DispatchConstants.PARSE_XML_PATH) + .parseScript("//item") + .build(); + Metrics metrics = Metrics.builder() + .http(http) + .aliasFields(Lists.newArrayList("name")) + .build(); + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + + httpCollectImpl.collect(builder, metrics); + + assertEquals(1, builder.getValuesCount()); + assertEquals("café", builder.getValues(0).getColumns(0)); + } finally { + server.stop(0); + } + } + + @Test + void parseResponseByXmlPathFallsBackToUtf8ForUnsupportedXmlEncoding() throws Exception { + String xmlResponse = """ + <?xml version="1.0" encoding="UNKNOWN-CHARSET"?> + <root> + <item> + <name>service</name> + </item> + </root> + """; + byte[] responseBytes = xmlResponse.getBytes(StandardCharsets.UTF_8); + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/metrics", exchange -> { + exchange.getResponseHeaders().add("Content-Type", "application/xml"); + exchange.sendResponseHeaders(200, responseBytes.length); + exchange.getResponseBody().write(responseBytes); + exchange.close(); + }); + server.start(); + + try { + HttpProtocol http = HttpProtocol.builder() + .method("GET") + .host("127.0.0.1") + .port(String.valueOf(server.getAddress().getPort())) + .url("/metrics") + .parseType(DispatchConstants.PARSE_XML_PATH) + .parseScript("//item") + .build(); + Metrics metrics = Metrics.builder() + .http(http) + .aliasFields(Lists.newArrayList("name")) + .build(); + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + + httpCollectImpl.collect(builder, metrics); + + assertEquals(1, builder.getValuesCount()); + assertEquals("service", builder.getValues(0).getColumns(0)); + } finally { + server.stop(0); + } + } + + @Test + void parseResponseByJsonPath() throws Exception { + String jsonResponse = "{" + + " \"name\": \"jvm.memory.used\"," + + " \"description\": \"The amount of used memory\"," + + " \"baseUnit\": \"bytes\"," + + " \"measurements\": [" + + " {" + + " \"statistic\": \"VALUE\"," + + " \"value\": 90282296" + + " }" + + " ]," + + " \"availableTags\": [" + + " {" + + " \"tag\": \"area\"," + + " \"values\": [" + + " \"heap\"," + + " \"nonheap\"" + + " ]" + + " }," + + " {" + + " \"tag\": \"id\"," + + " \"values\": [" + + " \"G1 Survivor Space\"," + + " \"G1 Eden Space\"" + + " ]" + + " }" + + " ]" + + "}"; + + HttpProtocol http = HttpProtocol.builder() + .parseType(DispatchConstants.PARSE_JSON_PATH) + .parseScript("$.availableTags[?(@.tag == \"id\")].values[*]") + .build(); + List<CollectRep.ValueRow> capturedRows = new ArrayList<>(); + CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() { + @Override + public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) { + capturedRows.add(valueRow); + return super.addValueRow(valueRow); + } + }; + Method parseMethod = HttpCollectImpl.class.getDeclaredMethod( + "parseResponseByJsonPath", + String.class, + List.class, + HttpProtocol.class, + CollectRep.MetricsData.Builder.class, + Long.class); + parseMethod.setAccessible(true); + + // Call the method + parseMethod.invoke(httpCollectImpl, jsonResponse, Lists.newArrayList("id"), http, builder, 100L); + + // Verify the results + assertEquals(2, capturedRows.size()); + CollectRep.ValueRow firstRow = capturedRows.get(0); + assertEquals("G1 Survivor Space", firstRow.getColumns(0)); + CollectRep.ValueRow secondRow = capturedRows.get(1); + assertEquals("G1 Eden Space", secondRow.getColumns(0)); + + // number + String numberJson = "{" + + " \"name\": \"system.cpu.usage\"," + + " \"description\": \"The \\\"recent cpu usage\\\" of the system the application is running in\"," + + " \"measurements\": [" + + " {" + + " \"statistic\": \"VALUE\"," + + " \"value\": 0.268751364291017" + + " }" + + " ]," + + " \"availableTags\": []" + + "}"; + http = HttpProtocol.builder() + .parseType(DispatchConstants.PARSE_JSON_PATH) + .parseScript("$.measurements[?(@.statistic == \"VALUE\")].value") + .build(); + capturedRows.clear(); + builder = new CollectRep.MetricsData.Builder() { + @Override + public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) { + capturedRows.add(valueRow); + return super.addValueRow(valueRow); + } + }; + parseMethod = HttpCollectImpl.class.getDeclaredMethod( + "parseResponseByJsonPath", + String.class, + List.class, + HttpProtocol.class, + CollectRep.MetricsData.Builder.class, + Long.class); + parseMethod.setAccessible(true); + + // Call the method + parseMethod.invoke(httpCollectImpl, numberJson, Lists.newArrayList("usage"), http, builder, 100L); + + // Verify the results + assertEquals(1, capturedRows.size()); + firstRow = capturedRows.get(0); + assertEquals("0.268751364291017", firstRow.getColumns(0)); + } + + @Test + void testParsePromQlLabelValue() throws Exception { + // Create Prometheus format test data + String prometheusData = """ + { + "status": "success", + "data": { + "resultType": "vector", + "result": [ + { + "metric": { + "__name__": "taos_cluster_info_first_ep", + "cluster_id": "590779086215866783", + "instance": "host.docker.internal:6043", + "job": "tdengine", + "value": "localhost:6030" + }, + "value": [ + 1756233177.048, + "1" + ] + } + ] + } + }"""; + List<CollectRep.ValueRow> capturedRows = new ArrayList<>(); + CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() { + @Override + public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) { + capturedRows.add(valueRow); + return super.addValueRow(valueRow); + } + + @Override + public String getMetrics() { + return "jvm_memory_used_bytes"; + } + }; + Method parseMethod = HttpCollectImpl.class.getDeclaredMethod( + "parseResponseByPromQl", + String.class, + List.class, + HttpProtocol.class, + CollectRep.MetricsData.Builder.class); + parseMethod.setAccessible(true); + + parseMethod.invoke(httpCollectImpl, prometheusData, Lists.newArrayList("cluster_id", "value"), new HttpProtocol(), builder); + + // Verify the results + assertEquals(1, capturedRows.size()); + CollectRep.ValueRow firstRow = capturedRows.get(0); + assertEquals("590779086215866783", firstRow.getColumns(0)); + assertEquals("localhost:6030", firstRow.getColumns(1)); + } + + @Test + void testParsePromQlMetricValue() throws Exception { + // Create Prometheus format test data + String prometheusData = """ + { + "status": "success", + "data": { + "resultType": "vector", + "result": [ + { + "metric": { + "__name__": "taos_cluster_info_first_ep", + "cluster_id": "590779086215866783", + "instance": "host.docker.internal:6043", + "job": "tdengine", + "value": "localhost:6030" + }, + "value": [ + 1756233177.048, + "1" + ] + } + ] + } + }"""; + List<CollectRep.ValueRow> capturedRows = new ArrayList<>(); + CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() { + @Override + public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) { + capturedRows.add(valueRow); + return super.addValueRow(valueRow); + } + + @Override + public String getMetrics() { + return "jvm_memory_used_bytes"; + } + }; + Method parseMethod = HttpCollectImpl.class.getDeclaredMethod( + "parseResponseByPromQl", + String.class, + List.class, + HttpProtocol.class, + CollectRep.MetricsData.Builder.class); + parseMethod.setAccessible(true); + + parseMethod.invoke(httpCollectImpl, prometheusData, Lists.newArrayList("cluster_id", "value", "metric_value"), new HttpProtocol(), builder); + + // Verify the results + assertEquals(1, capturedRows.size()); + CollectRep.ValueRow firstRow = capturedRows.get(0); + assertEquals("590779086215866783", firstRow.getColumns(0)); + assertEquals("localhost:6030", firstRow.getColumns(1)); + assertEquals("1", firstRow.getColumns(2)); + } + + @Test + void testParsePromQlValue() throws Exception { + // Create Prometheus format test data + String prometheusData = """ + { + "status": "success", + "data": { + "resultType": "vector", + "result": [ + { + "metric": { + "__name__": "taos_cluster_info_first_ep", + "cluster_id": "590779086215866783", + "instance": "host.docker.internal:6043", + "job": "tdengine" + }, + "value": [ + 1756233177.048, + "1" + ] + } + ] + } + }"""; + List<CollectRep.ValueRow> capturedRows = new ArrayList<>(); + CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() { + @Override + public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) { + capturedRows.add(valueRow); + return super.addValueRow(valueRow); + } + + @Override + public String getMetrics() { + return "jvm_memory_used_bytes"; + } + }; + Method parseMethod = HttpCollectImpl.class.getDeclaredMethod( + "parseResponseByPromQl", + String.class, + List.class, + HttpProtocol.class, + CollectRep.MetricsData.Builder.class); + parseMethod.setAccessible(true); + + parseMethod.invoke(httpCollectImpl, prometheusData, Lists.newArrayList("cluster_id", "value"), new HttpProtocol(), builder); + + // Verify the results + assertEquals(1, capturedRows.size()); + CollectRep.ValueRow firstRow = capturedRows.get(0); + assertEquals("590779086215866783", firstRow.getColumns(0)); + assertEquals("1", firstRow.getColumns(1)); + } + + @Test + void testParsePrometheusLabelValue() throws Exception { + // Create Prometheus format test data + String prometheusData = """ + # HELP jvm_memory_used_bytes The amount of used memory in bytes + # TYPE jvm_memory_used_bytes gauge + jvm_memory_used_bytes{area="heap",value="G1 Survivor Space"} 1048576 + """; + InputStream inputStream = new ByteArrayInputStream(prometheusData.getBytes(StandardCharsets.UTF_8)); + + List<CollectRep.ValueRow> capturedRows = new ArrayList<>(); + CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() { + @Override + public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) { + capturedRows.add(valueRow); + return super.addValueRow(valueRow); + } + + @Override + public String getMetrics() { + return "jvm_memory_used_bytes"; + } + }; + Method parseMethod = HttpCollectImpl.class.getDeclaredMethod( + "parseResponseByPrometheusExporter", + InputStream.class, + List.class, + CollectRep.MetricsData.Builder.class); + parseMethod.setAccessible(true); + + parseMethod.invoke(httpCollectImpl, inputStream, Lists.newArrayList("area", "value"), builder); + + // Verify the results + assertEquals(1, capturedRows.size()); + CollectRep.ValueRow firstRow = capturedRows.get(0); + assertEquals("heap", firstRow.getColumns(0)); + assertEquals("G1 Survivor Space", firstRow.getColumns(1)); + } + + @Test + void testParsePrometheus() throws Exception { + // Create Prometheus format test data + String prometheusData = """ + # HELP jvm_memory_used_bytes The amount of used memory + # TYPE jvm_memory_used_bytes gauge + jvm_memory_used_bytes{area="heap",id="G1 Eden Space"} 1.63577856E8 + jvm_memory_used_bytes{area="heap",id="G1 Old Gen"} 2.7874304E7 + jvm_memory_used_bytes{area="heap",id="G1 Survivor Space"} 512032.0 + jvm_memory_used_bytes{area="nonheap",id="CodeCache"} 1.460288E7 + jvm_memory_used_bytes{area="nonheap",id="Compressed Class Space"} 5844504.0 + jvm_memory_used_bytes{area="nonheap",id="Metaspace"} 4.1576344E7 + """; + InputStream inputStream = new ByteArrayInputStream(prometheusData.getBytes(StandardCharsets.UTF_8)); + + List<CollectRep.ValueRow> capturedRows = new ArrayList<>(); + CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() { + @Override + public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) { + capturedRows.add(valueRow); + return super.addValueRow(valueRow); + } + + @Override + public String getMetrics() { + return "jvm_memory_used_bytes"; + } + }; + Method parseMethod = HttpCollectImpl.class.getDeclaredMethod( + "parseResponseByPrometheusExporter", + InputStream.class, + List.class, + CollectRep.MetricsData.Builder.class); + parseMethod.setAccessible(true); + + parseMethod.invoke(httpCollectImpl, inputStream, Lists.newArrayList("area", "id"), builder); + + // Verify the results + assertEquals(6, capturedRows.size()); + CollectRep.ValueRow firstRow = capturedRows.get(0); + assertEquals("heap", firstRow.getColumns(0)); + assertEquals("G1 Eden Space", firstRow.getColumns(1)); + capturedRows.forEach(t -> assertEquals(2, t.getColumnsList().size())); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/http/SslCertificateCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/http/SslCertificateCollectImplTest.java new file mode 100644 index 0000000..263a7db --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/http/SslCertificateCollectImplTest.java @@ -0,0 +1,39 @@ +/* + * 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.collector.collect.http; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link SslCertificateCollectImpl} + */ +class SslCertificateCollectImplTest { + + @BeforeEach + void setUp() { + } + + @Test + void getInstance() { + } + + @Test + void collect() { + } +} \ No newline at end of file diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/icmp/IcmpCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/icmp/IcmpCollectImplTest.java new file mode 100644 index 0000000..133d3db --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/icmp/IcmpCollectImplTest.java @@ -0,0 +1,144 @@ +/* + * 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.collector.collect.icmp; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.IcmpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link IcmpCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +class IcmpCollectImplTest { + + @Mock + IcmpProtocol icmpProtocol; + + @Mock + Metrics metrics; + + @Mock + InetAddress inetAddress; + + @Mock + CollectRep.MetricsData.Builder builder; + + @InjectMocks + private IcmpCollectImpl icmpCollect; + + @BeforeEach + void setUp() { + icmpProtocol = IcmpProtocol.builder() + .host("127.0.0.1") + .timeout("3000") + .build(); + List<String> aliasField = new ArrayList<>(); + aliasField.add("responseTime"); + metrics = new Metrics(); + metrics.setName("test"); + metrics.setIcmp(icmpProtocol); + metrics.setAliasFields(aliasField); + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void testPreCheck() { + assertDoesNotThrow(() -> icmpCollect.preCheck(metrics)); + assertThrows(IllegalArgumentException.class, () -> icmpCollect.preCheck(null)); + metrics.setIcmp(null); + assertThrows(IllegalArgumentException.class, () -> icmpCollect.preCheck(null)); + } + + @Test + void testCollect() throws Exception { + try (MockedStatic<InetAddress> mockedInetAddress = Mockito.mockStatic(InetAddress.class)) { + mockedInetAddress.when(() -> InetAddress.getByName(Mockito.anyString())).thenReturn(inetAddress); + Mockito.when(inetAddress.isReachable(Mockito.anyInt())).thenReturn(true); + assertDoesNotThrow(() -> icmpCollect.collect(builder, metrics)); + assertEquals(1, builder.getValuesCount()); + assertNotNull(builder.getValues(0).getColumns(0)); + + } + } + + @Test + void testUnreachable() throws Exception { + try (MockedStatic<InetAddress> mockedInetAddress = Mockito.mockStatic(InetAddress.class)) { + mockedInetAddress.when(() -> InetAddress.getByName(Mockito.anyString())).thenReturn(inetAddress); + Mockito.when(inetAddress.isReachable(Mockito.anyInt())).thenReturn(false); + assertDoesNotThrow(() -> icmpCollect.collect(builder, metrics)); + assertEquals(CollectRep.Code.UN_REACHABLE, builder.getCode()); + assertNotNull(builder.getMsg()); + } + } + + @Test + void testUnknownHostException() { + try (MockedStatic<InetAddress> mockedInetAddress = Mockito.mockStatic(InetAddress.class)) { + mockedInetAddress.when(() -> InetAddress.getByName(Mockito.anyString())).thenThrow(new UnknownHostException("Mocked exception")); + assertDoesNotThrow(() -> icmpCollect.collect(builder, metrics)); + assertEquals(CollectRep.Code.UN_REACHABLE, builder.getCode()); + assertNotNull(builder.getMsg()); + } + } + + @Test + void testIoException() throws Exception { + try (MockedStatic<InetAddress> mockedInetAddress = Mockito.mockStatic(InetAddress.class)) { + mockedInetAddress.when(() -> InetAddress.getByName(Mockito.anyString())).thenReturn(inetAddress); + Mockito.when(inetAddress.isReachable(Mockito.anyInt())).thenThrow(new IOException("Mocked exception")); + assertDoesNotThrow(() -> icmpCollect.collect(builder, metrics)); + assertEquals(CollectRep.Code.UN_REACHABLE, builder.getCode()); + assertNotNull(builder.getMsg()); + } + } + + @Test + void testException() throws Exception { + try (MockedStatic<InetAddress> mockedInetAddress = Mockito.mockStatic(InetAddress.class)) { + mockedInetAddress.when(() -> InetAddress.getByName(Mockito.anyString())).thenReturn(inetAddress); + Mockito.when(inetAddress.isReachable(Mockito.anyInt())).thenThrow(new RuntimeException("Mocked exception")); + assertDoesNotThrow(() -> icmpCollect.collect(builder, metrics)); + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + assertNotNull(builder.getMsg()); + } + } + + @Test + void testSupportProtocol() { + assertEquals("icmp", icmpCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/imap/ImapCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/imap/ImapCollectImplTest.java new file mode 100644 index 0000000..18d246e --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/imap/ImapCollectImplTest.java @@ -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.collector.collect.imap; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.List; +import org.apache.commons.net.imap.IMAPClient; +import org.apache.commons.net.imap.IMAPSClient; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.ImapProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link ImapCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +public class ImapCollectImplTest { + private Metrics metrics; + private CollectRep.MetricsData.Builder builder; + @Mock + private ImapProtocol imapProtocol; + @InjectMocks + private ImapCollectImpl imapCollect; + + @BeforeEach + void setUp() { + imapProtocol = ImapProtocol.builder() + .host("0.0.0.0") + .port("993") + .ssl("true") + .email("test@test.email.com") + .authorize("test") + .folderName("testFolder") + .timeout("6000") + .build(); + metrics = new Metrics(); + metrics.setName("testMailboxInfo"); + metrics.setImap(imapProtocol); + metrics.setAliasFields(List.of("responseTime", "testFolderTotalMessageCount", "testFolderRecentMessageCount", "testFolderUnseenMessageCount")); + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void preCheck() { + assertDoesNotThrow(() -> imapCollect.preCheck(metrics)); + assertThrows(NullPointerException.class, () -> imapCollect.preCheck(null)); + metrics.setImap(null); + assertThrows(NullPointerException.class, () -> imapCollect.preCheck(null)); + } + + @Test + void enableSslCollect() { + String response = "* STATUS \"testFolder\" (MESSAGES 3 RECENT 2 UNSEEN 1)"; + MockedConstruction<IMAPSClient> mocked = Mockito.mockConstruction(IMAPSClient.class, + (imapsClient, context) -> { + Mockito.doNothing().when(imapsClient).connect(Mockito.anyString(), Mockito.anyInt()); + Mockito.doAnswer(invocationOnMock -> true).when(imapsClient).login(Mockito.anyString(), Mockito.anyString()); + Mockito.doAnswer(invocationOnMock -> true).when(imapsClient).isConnected(); + Mockito.when(imapsClient.sendCommand(Mockito.anyString())).thenReturn(0); + Mockito.when(imapsClient.getReplyString()).thenReturn(response); + Mockito.doAnswer(invocationOnMock -> true).when(imapsClient).logout(); + Mockito.doNothing().when(imapsClient).disconnect(); + }); + + imapCollect.preCheck(metrics); + imapCollect.collect(builder, metrics); + assertEquals(1, builder.getValuesCount()); + for (CollectRep.ValueRow valueRow : builder.getValuesList()) { + assertNotNull(valueRow.getColumns(0)); + assertEquals("3", valueRow.getColumns(1)); + assertEquals("2", valueRow.getColumns(2)); + assertEquals("1", valueRow.getColumns(3)); + + } + mocked.close(); + } + + @Test + void disableSslCollect() { + metrics.getImap().setSsl("false"); + String response = "* STATUS \"testFolder\" (MESSAGES 3 RECENT 2 UNSEEN 1)"; + MockedConstruction<IMAPClient> mocked = Mockito.mockConstruction(IMAPClient.class, + (imapClient, context) -> { + Mockito.doNothing().when(imapClient).connect(Mockito.anyString(), Mockito.anyInt()); + Mockito.doAnswer(invocationOnMock -> true).when(imapClient).login(Mockito.anyString(), Mockito.anyString()); + Mockito.doAnswer(invocationOnMock -> true).when(imapClient).isConnected(); + Mockito.when(imapClient.sendCommand(Mockito.anyString())).thenReturn(0); + Mockito.when(imapClient.getReplyString()).thenReturn(response); + Mockito.doAnswer(invocationOnMock -> true).when(imapClient).logout(); + Mockito.doNothing().when(imapClient).disconnect(); + }); + + imapCollect.preCheck(metrics); + imapCollect.collect(builder, metrics); + assertEquals(1, builder.getValuesCount()); + for (CollectRep.ValueRow valueRow : builder.getValuesList()) { + assertNotNull(valueRow.getColumns(0)); + assertEquals("3", valueRow.getColumns(1)); + assertEquals("2", valueRow.getColumns(2)); + assertEquals("1", valueRow.getColumns(3)); + + } + mocked.close(); + } + + @Test + void supportProtocol() { + assertEquals("imap", imapCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ipmi/IpmiSessionWrapperTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ipmi/IpmiSessionWrapperTest.java new file mode 100644 index 0000000..1c4c473 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ipmi/IpmiSessionWrapperTest.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ipmi; + + +import java.nio.ByteBuffer; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.Ipmi20Ipv4SessionWrapper; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.command.chassis.GetChassisStatusResponse; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RakpMessage1; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RakpMessage2; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RakpMessage3; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RakpMessage4; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.payload.RmcpPlusOpenSessionRequest; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.rmcp.RmcpPacket; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link Ipmi20Ipv4SessionWrapper} + */ +public class IpmiSessionWrapperTest { + + IpmiSession ipmiSession; + + @BeforeEach + void setUp() { + ipmiSession = new IpmiSession(0xA0A2A3A4); + ipmiSession.setUserName("root"); + ipmiSession.setPassword("calvin"); + } + + @Test + void testRmcpPlusOpenSessionRequest() { + RmcpPlusOpenSessionRequest request = new RmcpPlusOpenSessionRequest(); + Ipmi20Ipv4SessionWrapper wrapper = new Ipmi20Ipv4SessionWrapper(); + wrapper.setIpmiPayload(request); + RmcpPacket packet = new RmcpPacket(); + packet.withData(wrapper); + int length = packet.getWireLength(ipmiSession); + ByteBuffer buffer = ByteBuffer.allocate(length); + packet.toWire(ipmiSession, buffer); + buffer.flip(); + byte[] targetBytes = new byte[] { + 0x06, 0x00, (byte) 0xFF, 0x07, 0x06, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, (byte) 0xA4, (byte) 0xA3, (byte) 0xA2, (byte) 0xA0, 0x00, 0x00, 0x00, + 0x08, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x08, + 0x01, 0x00, 0x00, 0x00 + }; + for (byte targetByte : targetBytes) { + assert targetByte == buffer.get(); + } + } + + @Test + void testRakp1() { + ipmiSession.generateConsoleRandomNumber(); + ipmiSession.setSystemSessionId(0x02000300); + ipmiSession.setConsoleRandomNumber(new byte[]{0x04, 0x79, (byte) 0xfe, (byte) 0xe3, (byte) 0xe0, (byte) 0xa2, 0x67, 0x0a, 0x11, (byte) 0xd3, 0x3f, 0x0e, 0x48, (byte) 0xbe, 0x62, (byte) 0xb8}); + RakpMessage1 request = new RakpMessage1(); + Ipmi20Ipv4SessionWrapper wrapper = new Ipmi20Ipv4SessionWrapper(); + wrapper.setIpmiPayload(request); + RmcpPacket packet = new RmcpPacket(); + packet.withData(wrapper); + int length = packet.getWireLength(ipmiSession); + ByteBuffer buffer = ByteBuffer.allocate(length); + packet.toWire(ipmiSession, buffer); + buffer.flip(); + byte[] targetBytes = new byte[] { + 0x06, 0x00, (byte) 0xFF, 0x07, 0x06, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x04, 0x79, (byte) 0xFE, (byte) 0xE3, (byte) 0xE0, + (byte) 0xA2, 0x67, 0x0A, 0x11, (byte) 0xD3, 0x3F, 0x0E, 0x48, (byte) 0xBE, 0x62, (byte) 0xB8, 0x14, + 0x00, 0x00, 0x04, 0x72, 0x6F, 0x6F, 0x74 + }; + for (byte targetByte : targetBytes) { + assert targetByte == buffer.get(); + } + } + + @Test + void testRakp2() { + byte[] rakp2 = new byte[] { + 0x06, 0x00, (byte) 0xFF, 0x07, 0x06, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x00, 0x00, (byte) 0xA4, (byte) 0xA3, (byte) 0xA2, (byte) 0xA0, (byte) 0xA0, (byte) 0xEB, (byte) 0x3B, (byte) 0xBB, + (byte) 0xA9, 0x62, (byte) 0xDB, 0x2A, 0x27, 0x5F, 0x64, 0x6B, (byte) 0xFF, (byte) 0x8A, (byte) 0xC6, + (byte) 0xCF, 0x44, 0x45, 0x4C, 0x4C, 0x39, 0x00, 0x10, 0x38, (byte) 0x80, 0x38, (byte) 0xC4, (byte) 0xC0, 0x4F, + (byte) 0x35, 0x58, 0x31, (byte) 0xE4, (byte) 0xF8, 0x46, 0x42, 0x33, (byte) 0x8B, 0x67, (byte) 0xA8, + 0x3F, 0x4E, (byte) 0xCF, 0x3B, 0x44, (byte) 0xCC, 0x0D, 0x70, (byte) 0xEF, 0x2B, (byte) 0xBD, (byte) 0x92 + }; + ipmiSession.setSystemSessionId(0x02000300); + byte[] consoleRandom = new byte[]{ + 0x04, 0x79, (byte) 0xfe, (byte) 0xe3, (byte) 0xe0, (byte) 0xa2, 0x67, 0x0a, 0x11, (byte) 0xd3, 0x3f, + 0x0e, 0x48, (byte) 0xbe, 0x62, (byte) 0xb8 + }; + ipmiSession.setConsoleRandomNumber(consoleRandom); + RmcpPacket packet = new RmcpPacket(); + packet.fromWire(ipmiSession, ByteBuffer.wrap(rakp2)); + RakpMessage2 rakpMessage2 = packet.getEncapsulated(RakpMessage2.class); + byte[] systemRandomNumber = new byte[] { + (byte) 0xA0, (byte) 0xEB, 0x3B, (byte) 0xBB, (byte) 0xA9, 0x62, + (byte) 0xDB, 0x2A, 0x27, 0x5F, 0x64, 0x6B, (byte) 0xFF, (byte) 0x8A, (byte) 0xC6, (byte) 0xCF + }; + byte[] guid = new byte[] { + 0x44, 0x45, 0x4C, 0x4C, 0x39, 0x00, 0x10, 0x38, (byte) 0x80, 0x38, (byte) 0xC4, + (byte) 0xC0, 0x4F, (byte) 0x35, 0x58, 0x31 + }; + for (int i = 0; i < systemRandomNumber.length; i++) { + assert systemRandomNumber[i] == rakpMessage2.systemRandom[i]; + } + for (int i = 0; i < guid.length; i++) { + assert guid[i] == rakpMessage2.systemGuid[i]; + } + assert rakpMessage2.consoleSessionId == ipmiSession.getConsoleSessionId(); + } + + @Test + void testRakp3() { + byte[] systemRandomNumber = new byte[] { + (byte) 0xA0, (byte) 0xEB, 0x3B, (byte) 0xBB, (byte) 0xA9, 0x62, + (byte) 0xDB, 0x2A, 0x27, 0x5F, 0x64, 0x6B, (byte) 0xFF, (byte) 0x8A, (byte) 0xC6, (byte) 0xCF + }; + byte[] guid = new byte[] { + 0x44, 0x45, 0x4C, 0x4C, 0x39, 0x00, 0x10, 0x38, (byte) 0x80, 0x38, (byte) 0xC4, + (byte) 0xC0, 0x4F, (byte) 0x35, 0x58, 0x31 + }; + ipmiSession.setSystemRandomNumber(systemRandomNumber); + ipmiSession.setSystemGuid(guid); + ipmiSession.setSystemSessionId(0x02000300); + RakpMessage3 request = new RakpMessage3(); + Ipmi20Ipv4SessionWrapper wrapper = new Ipmi20Ipv4SessionWrapper(); + wrapper.setIpmiPayload(request); + RmcpPacket packet = new RmcpPacket(); + packet.withData(wrapper); + int length = packet.getWireLength(ipmiSession); + ByteBuffer buffer = ByteBuffer.allocate(length); + packet.toWire(ipmiSession, buffer); + buffer.flip(); + byte[] targetBytes = new byte[] { + 0x06, 0x00, (byte) 0xFF, 0x07, 0x06, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x6D, (byte) 0x86, (byte) 0xE3, 0x11, (byte) 0xEB, + (byte) 0xB5, (byte) 0xBE, 0x51, (byte) 0x80, 0x16, (byte) 0xFD, 0x14, 0x35, 0x73, 0x3E, (byte) 0xAD, + (byte) 0xBC, (byte) 0xB1, (byte) 0xC4, 0x66 + + }; + for (byte targetByte : targetBytes) { + assert targetByte == buffer.get(); + } + } + + @Test + void testRakp4() { + byte[] rakp4 = new byte[] { + 0x06, 0x00, (byte) 0xFF, 0x07, 0x06, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, + 0x00, 0x00, 0x00, (byte) 0xA4, (byte) 0xA3, (byte) 0xA2, (byte) 0xA0, (byte) 0x9C, 0x7C, (byte) 0xEB, + 0x17, (byte) 0xC7, (byte) 0xCA, 0x43, 0x3B, (byte) 0xB0, 0x61, (byte) 0xBA, 0x6D + }; + byte[] consoleRandom = new byte[]{ + 0x04, 0x79, (byte) 0xfe, (byte) 0xe3, (byte) 0xe0, (byte) 0xa2, 0x67, 0x0a, 0x11, (byte) 0xd3, 0x3f, + 0x0e, 0x48, (byte) 0xbe, 0x62, (byte) 0xb8 + }; + ipmiSession.setConsoleRandomNumber(consoleRandom); + byte[] systemRandomNumber = new byte[] { + (byte) 0xA0, (byte) 0xEB, 0x3B, (byte) 0xBB, (byte) 0xA9, 0x62, + (byte) 0xDB, 0x2A, 0x27, 0x5F, 0x64, 0x6B, (byte) 0xFF, (byte) 0x8A, (byte) 0xC6, (byte) 0xCF + }; + ipmiSession.setSystemRandomNumber(systemRandomNumber); + ipmiSession.setSystemSessionId(0x02000300); + byte[] guid = new byte[] { + 0x44, 0x45, 0x4C, 0x4C, 0x39, 0x00, 0x10, 0x38, (byte) 0x80, 0x38, (byte) 0xC4, + (byte) 0xC0, 0x4F, (byte) 0x35, 0x58, 0x31 + }; + ipmiSession.setSystemGuid(guid); + ipmiSession.generateSik(); + RmcpPacket packet = new RmcpPacket(); + packet.fromWire(ipmiSession, ByteBuffer.wrap(rakp4)); + RakpMessage4 rakpMessage4 = packet.getEncapsulated(RakpMessage4.class); + assert rakpMessage4.consoleSessionId == ipmiSession.getConsoleSessionId(); + } + + @Test + void testIpmiMessage() { + byte[] consoleRandom = new byte[]{ + 0x04, 0x79, (byte) 0xfe, (byte) 0xe3, (byte) 0xe0, (byte) 0xa2, 0x67, 0x0a, 0x11, (byte) 0xd3, 0x3f, + 0x0e, 0x48, (byte) 0xbe, 0x62, (byte) 0xb8 + }; + ipmiSession.setConsoleRandomNumber(consoleRandom); + byte[] systemRandomNumber = new byte[] { + (byte) 0xA0, (byte) 0xEB, 0x3B, (byte) 0xBB, (byte) 0xA9, 0x62, + (byte) 0xDB, 0x2A, 0x27, 0x5F, 0x64, 0x6B, (byte) 0xFF, (byte) 0x8A, (byte) 0xC6, (byte) 0xCF + }; + ipmiSession.setSystemRandomNumber(systemRandomNumber); + ipmiSession.setSystemSessionId(0x02000300); + byte[] guid = new byte[] { + 0x44, 0x45, 0x4C, 0x4C, 0x39, 0x00, 0x10, 0x38, (byte) 0x80, 0x38, (byte) 0xC4, + (byte) 0xC0, 0x4F, (byte) 0x35, 0x58, 0x31 + }; + ipmiSession.setSystemGuid(guid); + ipmiSession.generateSik(); + ipmiSession.setK1(ipmiSession.generateK(1)); + ipmiSession.setK2(ipmiSession.generateK(2)); + + byte[] message = { + (byte) 0x06, (byte) 0x00, (byte) 0xff, (byte) 0x07, (byte) 0x06, (byte) 0xc0, (byte) 0xa4, (byte) 0xa3, + (byte) 0xa2, (byte) 0xa0, (byte) 0x06, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x20, (byte) 0x00, + (byte) 0xa0, (byte) 0xeb, (byte) 0x3b, (byte) 0xbb, (byte) 0xa9, (byte) 0x62, (byte) 0xdb, (byte) 0x2a, + (byte) 0x27, (byte) 0x5f, (byte) 0x64, (byte) 0x6b, (byte) 0xff, (byte) 0x8a, (byte) 0xc6, (byte) 0xcf, + (byte) 0x4e, (byte) 0x4e, (byte) 0xe1, (byte) 0xab, (byte) 0xcc, (byte) 0xf7, (byte) 0x72, (byte) 0x09, + (byte) 0x23, (byte) 0x63, (byte) 0xd3, (byte) 0xec, (byte) 0xb6, (byte) 0xdb, (byte) 0xf1, (byte) 0xb7, + (byte) 0xff, (byte) 0xff, (byte) 0x02, (byte) 0x07, (byte) 0x68, (byte) 0x3b, (byte) 0x0e, (byte) 0x61, + (byte) 0x8e, (byte) 0xd6, (byte) 0x6b, (byte) 0x3d, (byte) 0x32, (byte) 0x58, (byte) 0xf1, (byte) 0x1d + }; + + RmcpPacket packet = new RmcpPacket(); + packet.fromWire(ipmiSession, ByteBuffer.wrap(message)); + GetChassisStatusResponse response = packet.getEncapsulated(GetChassisStatusResponse.class); + + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ipmi/common/AbstractWireableTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ipmi/common/AbstractWireableTest.java new file mode 100644 index 0000000..acf0c4d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ipmi/common/AbstractWireableTest.java @@ -0,0 +1,47 @@ +/* + * 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.collector.collect.ipmi.common; + +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.common.AbstractWireable; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link AbstractWireable} + */ +public class AbstractWireableTest { + + public static final byte b = 0b01010101; + + @Test + void testSetBits() { + byte result = AbstractWireable.setBits(b, 0, 0x03, 0x02); + assert result == 0b01010110; + + result = AbstractWireable.setBits(b, 0x03, 0x02); + assert result == 0b01010110; + + result = AbstractWireable.setBits(b, 0x02); + assert result == 0b00000010; + + result = AbstractWireable.setBits(b, 2, 0x03, 0x02); + assert result == 0b01011001; + + result = AbstractWireable.setBits(b, 2, 0x03, 0x04); + assert result == 0b01010001; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ipmi/security/IpmiConfidentialityTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ipmi/security/IpmiConfidentialityTest.java new file mode 100644 index 0000000..7e1f487 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ipmi/security/IpmiConfidentialityTest.java @@ -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.collector.collect.ipmi.security; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import org.apache.hertzbeat.collector.collect.ipmi2.client.IpmiSession; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.IpmiConfidentialityCode; +import org.apache.hertzbeat.collector.collect.ipmi2.protocol.ipmi.security.confidentiality.IpmiConfidentiality; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link IpmiConfidentiality} + */ +public class IpmiConfidentialityTest { + + IpmiSession session; + + @BeforeEach + public void setUp() { + session = new IpmiSession(1); + session.setK2(new byte[]{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10}); + + } + + @Test + public void testAesCbc128() throws Exception { + String message = "Hello World!"; + IpmiConfidentialityCode code = IpmiConfidentialityCode.AES_CBC_128; + IpmiConfidentiality confidentiality = code.newIpmiConfidentiality(); + ByteBuffer in = ByteBuffer.allocate(1024); + ByteBuffer out = ByteBuffer.allocate(1024); + in.put(message.getBytes(StandardCharsets.UTF_8)); + in.flip(); + assert confidentiality != null; + confidentiality.encrypt(session, in, out); + out.flip(); + ByteBuffer decrypt = confidentiality.decrypt(session, out); + assert message.equals(asString(decrypt)); + } + + private static String asString(final ByteBuffer buffer) { + final ByteBuffer copy = buffer.duplicate(); + final byte[] bytes = new byte[copy.remaining()]; + copy.get(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/jmx/JmxCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/jmx/JmxCollectImplTest.java new file mode 100644 index 0000000..539d600 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/jmx/JmxCollectImplTest.java @@ -0,0 +1,92 @@ +/* + * 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.collector.collect.jmx; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JmxProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link JmxCollectImpl} + */ +class JmxCollectImplTest { + private JmxCollectImpl jmxCollect; + + @BeforeEach + void setUp() throws Exception { + jmxCollect = new JmxCollectImpl(); + } + + @Test + void preCheck() throws IllegalArgumentException { + // metrics is null, will throw exception + assertThrows(IllegalArgumentException.class, () -> { + jmxCollect.preCheck(null); + }); + + // should not contain /stub/ + assertThrows(IllegalArgumentException.class, () -> { + JmxProtocol jmx = JmxProtocol.builder().build(); + jmx.setUrl("/stub/"); + Metrics metrics = Metrics.builder().jmx(jmx).build(); + + jmxCollect.preCheck(metrics); + }); + } + + @Test + void collect() { + // metrics is null + assertDoesNotThrow(() -> { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + jmxCollect.collect(builder, null); + }); + } + + @Test + void supportProtocol() { + assert DispatchConstants.PROTOCOL_JMX.equals(jmxCollect.supportProtocol()); + } + + @Test + void preCheckShouldAcceptSupportedJmxRmiUrl() { + JmxProtocol jmx = JmxProtocol.builder() + .url("service:jmx:rmi:///jndi/rmi://127.0.0.1:9999/jmxrmi") + .build(); + Metrics metrics = Metrics.builder().jmx(jmx).build(); + + assertDoesNotThrow(() -> jmxCollect.preCheck(metrics)); + } + + @Test + void preCheckShouldRejectUnsafeJndiProtocolInJmxRmiUrl() { + JmxProtocol jmx = JmxProtocol.builder() + .url("service:jmx:rmi:///jndi/ldap://127.0.0.1:1389/jmxrmi") + .build(); + Metrics metrics = Metrics.builder().jmx(jmx).build(); + + assertThrows(IllegalArgumentException.class, () -> jmxCollect.preCheck(metrics)); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/memcached/MemcachedCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/memcached/MemcachedCollectImplTest.java new file mode 100644 index 0000000..019f803 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/memcached/MemcachedCollectImplTest.java @@ -0,0 +1,110 @@ +/* + * 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.collector.collect.memcached; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.MemcachedProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link MemcachedCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +public class MemcachedCollectImplTest { + @Mock + MemcachedProtocol memcachedProtocol; + + @InjectMocks + private MemcachedCollectImpl memcachedCollect; + + private Metrics metrics; + + private CollectRep.MetricsData.Builder builder; + + @BeforeEach + void setUp() { + memcachedProtocol = MemcachedProtocol.builder() + .host("0.0.0.0") + .port("11211") + .build(); + metrics = new Metrics(); + metrics.setName("server_info"); + metrics.setMemcached(memcachedProtocol); + metrics.setAliasFields(List.of("responseTime", "pid", "uptime", "item_size", "item_count", "curr_items")); + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void testPreCheck() { + assertDoesNotThrow(() -> memcachedCollect.preCheck(metrics)); + assertThrows(IllegalArgumentException.class, () -> memcachedCollect.preCheck(null)); + metrics.setIcmp(null); + assertThrows(IllegalArgumentException.class, () -> memcachedCollect.preCheck(null)); + } + + @Test + void testCollectCmdResponse() { + String httpResponse = + """ + STAT pid 1 + STAT uptime 2 + END + """; + OutputStream outputStreamMock = Mockito.mock(OutputStream.class); + byte[] responseBytes = httpResponse.getBytes(StandardCharsets.UTF_8); + InputStream inputStream = new ByteArrayInputStream(responseBytes); + MockedConstruction<Socket> mocked = + Mockito.mockConstruction(Socket.class, (socket, context) -> { + Mockito.when(socket.isConnected()).thenReturn(true); + Mockito.when(socket.getOutputStream()).thenReturn(outputStreamMock); + Mockito.when(socket.getInputStream()).thenReturn(inputStream); + }); + + memcachedCollect.collect(builder, metrics); + assertEquals(1, builder.getValuesCount()); + for (CollectRep.ValueRow valueRow : builder.getValuesList()) { + assertNotNull(valueRow.getColumns(0)); + assertEquals(valueRow.getColumns(1), "1"); + assertEquals(valueRow.getColumns(2), "2"); + } + mocked.close(); + } + + @Test + void testSupportProtocol() { + assertEquals("memcached", memcachedCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/modbus/ModbusCollectTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/modbus/ModbusCollectTest.java new file mode 100644 index 0000000..77484c7 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/modbus/ModbusCollectTest.java @@ -0,0 +1,227 @@ +/* + * 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.collector.collect.modbus; + + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.ModbusProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test case for {@link ModbusCollectImpl} + */ +public class ModbusCollectTest { + private ModbusCollectImpl modbusCollect; + private Metrics metrics; + private CollectRep.MetricsData.Builder builder; + + @BeforeEach + public void setup() { + modbusCollect = new ModbusCollectImpl(); + ModbusProtocol modbus = ModbusProtocol.builder().build(); + metrics = Metrics.builder() + .modbus(modbus) + .build(); + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void preCheck() { + // host is empty + assertThrows(IllegalArgumentException.class, () -> { + modbusCollect.preCheck(metrics); + }); + + // port is empty default add 502 + assertThrows(IllegalArgumentException.class, () -> { + ModbusProtocol modbus = ModbusProtocol.builder().build(); + modbus.setHost("127.0.0.1"); + metrics.setModbus(modbus); + modbusCollect.preCheck(metrics); + }); + + // driverName version is empty + assertThrows(IllegalArgumentException.class, () -> { + ModbusProtocol modbus = ModbusProtocol.builder().build(); + modbus.setHost("127.0.0.1"); + modbus.setPort("502"); + metrics.setModbus(modbus); + modbusCollect.preCheck(metrics); + }); + + // addressSyntax is empty + assertThrows(IllegalArgumentException.class, () -> { + ModbusProtocol modbus = ModbusProtocol.builder().build(); + modbus.setHost("127.0.0.1"); + modbus.setPort("502"); + modbus.setDriverName("modbus-tcp"); + metrics.setModbus(modbus); + modbusCollect.preCheck(metrics); + }); + + // registerAddresses version is empty + assertThrows(IllegalArgumentException.class, () -> { + ModbusProtocol modbus = ModbusProtocol.builder().build(); + modbus.setHost("127.0.0.1"); + modbus.setPort("502"); + modbus.setDriverName("modbus-tcp"); + modbus.setAddressSyntax("holding-register"); + metrics.setModbus(modbus); + modbusCollect.preCheck(metrics); + }); + + // aliasFields is empty + assertThrows(IllegalArgumentException.class, () -> { + ModbusProtocol modbus = ModbusProtocol.builder().build(); + modbus.setHost("127.0.0.1"); + modbus.setPort("502"); + modbus.setDriverName("modbus-tcp"); + modbus.setAddressSyntax("holding-register"); + modbus.setRegisterAddresses(List.of("1")); + metrics.setModbus(modbus); + modbusCollect.preCheck(metrics); + }); + + // the number of aliasFields is not equal to registerAddresses(responseTime doesn't count) + assertThrows(IllegalArgumentException.class, () -> { + ModbusProtocol modbus = ModbusProtocol.builder().build(); + modbus.setHost("127.0.0.1"); + modbus.setPort("502"); + modbus.setDriverName("modbus-tcp"); + modbus.setAddressSyntax("holding-register"); + modbus.setRegisterAddresses(List.of("1", "2")); + metrics.setAliasFields(List.of( + "responseTime", + "holding-register:0" + )); + metrics.setModbus(modbus); + modbusCollect.preCheck(metrics); + }); + + // everything is ok + assertDoesNotThrow(() -> { + ModbusProtocol modbus = ModbusProtocol.builder().build(); + modbus.setHost("127.0.0.1"); + modbus.setPort("502"); + modbus.setDriverName("modbus-tcp"); + modbus.setAddressSyntax("holding-register"); + modbus.setRegisterAddresses(List.of("1", "2", "3[2]")); + modbus.setSlaveId("2"); + modbus.setTimeout("500"); + metrics.setModbus(modbus); + metrics.setAliasFields(List.of( + "responseTime", + "holding-register:0", + "holding-register:1", + "holding-register:2-0", + "holding-register:2-1" + )); + modbusCollect.preCheck(metrics); + }); + } + + @Test + void supportProtocol() { + Assertions.assertEquals(DispatchConstants.PROTOCOL_MODBUS, modbusCollect.supportProtocol()); + } + + @Test + void collect() { + // with holding-register + assertDoesNotThrow(() -> { + ModbusProtocol modbus = ModbusProtocol.builder().build(); + modbus.setHost("127.0.0.1"); + modbus.setPort("502"); + modbus.setDriverName("modbus-tcp"); + modbus.setAddressSyntax("holding-register"); + modbus.setRegisterAddresses(List.of("1", "2[3]")); + modbus.setSlaveId("1"); + modbus.setTimeout("500"); + + metrics.setModbus(modbus); + metrics.setAliasFields(List.of( + "responseTime", + "holding-register:0", + "holding-register:1-0", + "holding-register:1-1", + "holding-register:1-2" + )); + modbusCollect.preCheck(metrics); + modbusCollect.collect(builder, metrics); + }); + + // with coil + assertDoesNotThrow(() -> { + ModbusProtocol modbus = ModbusProtocol.builder().build(); + modbus.setHost("127.0.0.1"); + modbus.setPort("502"); + modbus.setDriverName("modbus-tcp"); + modbus.setAddressSyntax("coil"); + modbus.setRegisterAddresses(List.of("1", "2[3]")); + modbus.setSlaveId("1"); + modbus.setTimeout("500"); + + metrics.setModbus(modbus); + metrics.setAliasFields(List.of( + "responseTime", + "coil:0", + "coil:1-0", + "coil:1-1", + "coil:1-2" + )); + modbusCollect.preCheck(metrics); + modbusCollect.collect(builder, metrics); + }); + + + // with slaveId2 + // with holding-register + assertDoesNotThrow(() -> { + ModbusProtocol modbus = ModbusProtocol.builder().build(); + modbus.setHost("127.0.0.1"); + modbus.setPort("502"); + modbus.setDriverName("modbus-tcp"); + modbus.setAddressSyntax("holding-register"); + modbus.setRegisterAddresses(List.of("1", "2[3]")); + modbus.setSlaveId("2"); + modbus.setTimeout("500"); + + metrics.setModbus(modbus); + metrics.setAliasFields(List.of( + "responseTime", + "holding-register:0", + "holding-register:1-0", + "holding-register:1-1", + "holding-register:1-2" + )); + modbusCollect.preCheck(metrics); + modbusCollect.collect(builder, metrics); + }); + + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/mqtt/MqttCollectTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/mqtt/MqttCollectTest.java new file mode 100644 index 0000000..588cccc --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/mqtt/MqttCollectTest.java @@ -0,0 +1,107 @@ +/* + * 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.collector.collect.mqtt; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.MqttProtocol; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link MqttCollectImpl} + */ +class MqttCollectTest { + private MqttCollectImpl mqttCollect; + private Metrics metrics; + private MqttProtocol.MqttProtocolBuilder mqttBuilder; + + @BeforeEach + void setup() { + mqttCollect = new MqttCollectImpl(); + metrics = new Metrics(); + + // Initialize base MQTT parameters for test cases + mqttBuilder = MqttProtocol.builder() + .host("example.com") + .port("1883") + .protocol("mqtt") + .timeout("5000") + .keepalive("60"); + } + + // Region: preCheck validation tests + + @Test + // Verify preCheck throws exception when host is missing + void preCheckShouldThrowWhenHostMissing() { + metrics.setMqtt(mqttBuilder.host("").build()); + assertThrows(IllegalArgumentException.class, () -> mqttCollect.preCheck(metrics)); + } + + @Test + // Verify preCheck throws exception when port is missing + void preCheckShouldThrowWhenPortMissing() { + metrics.setMqtt(mqttBuilder.port("").build()); + assertThrows(IllegalArgumentException.class, () -> mqttCollect.preCheck(metrics)); + } + + @Test + // Verify preCheck throws exception when MQTTS mutual auth is enabled but CA cert is missing + void preCheckShouldThrowWhenMqttsMutualAuthMissingCerts() { + metrics.setMqtt(mqttBuilder + .protocol("mqtts") + .enableMutualAuth("true") + .caCert("") + .clientCert("client.crt") + .clientKey("client.key") + .build()); + assertThrows(IllegalArgumentException.class, () -> mqttCollect.preCheck(metrics)); + } + + @Test + // Verify preCheck succeeds with valid standard MQTT parameters + void preCheckShouldSucceedWithValidMqttParams() { + metrics.setMqtt(mqttBuilder.build()); + assertDoesNotThrow(() -> mqttCollect.preCheck(metrics)); + } + + @Test + // Verify preCheck succeeds with valid MQTTS parameters including mutual authentication + void preCheckShouldSucceedWithValidMqttsMutualAuth() { + metrics.setMqtt(mqttBuilder + .protocol("mqtts") + .enableMutualAuth("true") + .caCert("ca.pem") + .clientCert("client.crt") + .clientKey("client.key") + .build()); + assertDoesNotThrow(() -> mqttCollect.preCheck(metrics)); + } + // End region + + @Test + // Verify supportProtocol method returns correct MQTT constant + void supportProtocolShouldReturnMqttConstant() { + assertEquals(DispatchConstants.PROTOCOL_MQTT, mqttCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/nginx/NginxCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/nginx/NginxCollectImplTest.java new file mode 100644 index 0000000..030ee64 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/nginx/NginxCollectImplTest.java @@ -0,0 +1,414 @@ +/* + * 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.collector.collect.nginx; + +import static org.apache.hertzbeat.common.constants.CommonConstants.TYPE_STRING; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.NginxProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicStatusLine; +import org.apache.http.protocol.HttpContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link NginxCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +public class NginxCollectImplTest { + + @InjectMocks + private NginxCollectImpl nginxCollect; + + @Mock + private CloseableHttpClient client; + + @Mock + private CloseableHttpResponse mockHttpResponse; + + @BeforeEach + public void setup() { + } + + @Test + void preCheck() { + // metrics is null + assertThrows(IllegalArgumentException.class, () -> { + nginxCollect.preCheck(null); + }); + + // nginx protocol is null + assertThrows(IllegalArgumentException.class, () -> { + nginxCollect.preCheck(Metrics.builder().build()); + }); + + // nginx protocol is invalid + assertThrows(IllegalArgumentException.class, () -> { + nginxCollect.preCheck(Metrics.builder().nginx(NginxProtocol.builder().build()).build()); + }); + } + + @Test + public void testNginxCollectFail() throws IOException { + NginxProtocol nginxProtocol = NginxProtocol.builder() + .host("127.0.0.1") + .port("8080") + .timeout("6000") + .url("/nginx-status") + .build(); + + try (MockedStatic<CommonHttpClient> mockStatic = Mockito.mockStatic(CommonHttpClient.class)) { + mockStatic.when(CommonHttpClient::getHttpClient).thenReturn(client); + Mockito.when(client.execute(Mockito.any(HttpUriRequest.class), Mockito.any(HttpContext.class))) + .thenReturn(mockHttpResponse); + + StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("http", 1, 1), + 500, "fail"); + Mockito.when(mockHttpResponse.getStatusLine()).thenReturn(statusLine); + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + long monitorId = 999; + String app = "testNginx"; + Metrics metrics = new Metrics(); + metrics.setName("nginx_status"); + metrics.setNginx(nginxProtocol); + nginxCollect.preCheck(metrics); + nginxCollect.collect(builder, metrics); + assertEquals(builder.getCode(), CollectRep.Code.FAIL); + } + + } + + @Test + public void testNginxStatusCollect() throws IOException { + NginxProtocol nginxProtocol = NginxProtocol.builder() + .host("127.0.0.1") + .port("8080") + .timeout("6000") + .url("/nginx-status") + .build(); + + try (MockedStatic<CommonHttpClient> mockedStatic = Mockito.mockStatic(CommonHttpClient.class)) { + mockedStatic.when(CommonHttpClient::getHttpClient).thenReturn(client); + + Mockito.when(client.execute(Mockito.any(HttpUriRequest.class), Mockito.any(HttpContext.class))) + .thenReturn(mockHttpResponse); + + StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("http", 1, 1), + 200, "OK"); + Mockito.when(mockHttpResponse.getStatusLine()).thenReturn(statusLine); + + String responseTemp = """ + Active connections: %s\s + server accepts handled requests + 5 5 5\s + Reading: %s Writing: 1 Waiting: 1\s + """; + String connections = "2"; + String reading = "1"; + String response = String.format(responseTemp, connections, reading); + HttpEntity entity = new CustomHttpEntity(response, ContentType.create("text/plain", "UTF-8")); + Mockito.when(mockHttpResponse.getEntity()).thenReturn(entity); + + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + long monitorId = 999; + String app = "testNginx"; + + Metrics metrics = new Metrics(); + List<String> aliasField = new ArrayList<>(); + aliasField.add("active"); + aliasField.add("reading"); + + List<Metrics.Field> fields = new ArrayList<>(); + fields.add(Metrics.Field.builder() + .field("active") + .type(TYPE_STRING) + .build()); + fields.add(Metrics.Field.builder() + .field("reading") + .type(TYPE_STRING) + .build()); + + metrics.setAliasFields(aliasField); + metrics.setFields(fields); + metrics.setName("nginx_status"); + metrics.setNginx(nginxProtocol); + nginxCollect.preCheck(metrics); + nginxCollect.collect(builder, metrics); + assertEquals(builder.getCode(), CollectRep.Code.SUCCESS); + for (CollectRep.ValueRow row : builder.getValuesList()) { + assertEquals(row.getColumnsCount(), 2); + assertEquals(row.getColumns(0), connections); + assertEquals(row.getColumns(1), reading); + } + } + } + + @Test + public void testNginxReqStatusCollect() throws IOException { + NginxProtocol nginxProtocol = NginxProtocol.builder() + .host("127.0.0.1") + .port("8080") + .timeout("6000") + .url("/req-status") + .build(); + + try (MockedStatic<CommonHttpClient> mockedStatic = Mockito.mockStatic(CommonHttpClient.class)) { + mockedStatic.when(CommonHttpClient::getHttpClient).thenReturn(client); + + Mockito.when(client.execute(Mockito.any(HttpUriRequest.class), Mockito.any(HttpContext.class))) + .thenReturn(mockHttpResponse); + + StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("http", 1, 1), + 200, "OK"); + Mockito.when(mockHttpResponse.getStatusLine()).thenReturn(statusLine); + + String responseTemp = """ + zone_name key max_active max_bw traffic requests active bandwidth + imgstore_appid 43 27 6M 63G %s 0 %s + imgstore_appid 53 329 87M 2058G %s 50 %s + """; + String request0 = "374063"; + String bandwidth0 = "0"; + String request1 = "7870529"; + String bandwidth1 = "25M"; + String response = String.format(responseTemp, request0, bandwidth0, request1, bandwidth1); + HttpEntity entity = new CustomHttpEntity(response, ContentType.create("text/plain", "UTF-8")); + Mockito.when(mockHttpResponse.getEntity()).thenReturn(entity); + + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + long monitorId = 999; + String app = "testNginx"; + + Metrics metrics = new Metrics(); + List<String> aliasField = new ArrayList<>(); + aliasField.add("requests"); + aliasField.add("bandwidth"); + + List<Metrics.Field> fields = new ArrayList<>(); + fields.add(Metrics.Field.builder() + .field("requests") + .type(TYPE_STRING) + .build()); + fields.add(Metrics.Field.builder() + .field("bandwidth") + .type(TYPE_STRING) + .build()); + + metrics.setAliasFields(aliasField); + metrics.setFields(fields); + metrics.setName("req_status"); + metrics.setNginx(nginxProtocol); + nginxCollect.preCheck(metrics); + nginxCollect.collect(builder, metrics); + assertEquals(builder.getCode(), CollectRep.Code.SUCCESS); + assertEquals(builder.getValuesCount(), 2); + for (int i = 0; i < builder.getValuesList().size(); i++) { + CollectRep.ValueRow row = builder.getValues(i); + assertEquals(row.getColumnsCount(), 2); + if (i == 0) { + assertEquals(row.getColumns(0), request0); + assertEquals(row.getColumns(1), bandwidth0); + } else { + assertEquals(row.getColumns(0), request1); + assertEquals(row.getColumns(1), bandwidth1); + } + } + + } + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_NGINX, nginxCollect.supportProtocol()); + } + + static class CustomHttpEntity implements HttpEntity { + + private final String content; + private final ContentType contentType; + + public CustomHttpEntity(String content, ContentType contentType) { + this.content = content; + this.contentType = contentType; + } + + @Override + public boolean isRepeatable() { + return true; + } + + @Override + public boolean isChunked() { + return false; + } + + @Override + public boolean isStreaming() { + return false; + } + + @Override + public void consumeContent() { + + } + + @Override + public long getContentLength() { + return content.getBytes(contentType.getCharset()).length; + } + + @Override + public InputStream getContent() { + return new ByteArrayInputStream(content.getBytes(contentType.getCharset())); + } + + @Override + public void writeTo(OutputStream outStream) throws IOException { + outStream.write(content.getBytes(contentType.getCharset())); + outStream.flush(); + } + + @Override + public Header getContentEncoding() { + return null; + } + + @Override + public Header getContentType() { + return contentType != null ? new BasicHeader("Content-Type", contentType.toString()) : null; + } + + } + + + @Test + public void testNginxStatusMatch() { + String status = """ + Active connections: 2 + server accepts handled requests + 4 4 2 + Reading: 0 Writing: 1 Waiting: 1"""; + + // Use regular expressions to match and extract the required keys and corresponding values + Pattern keyValuePattern = Pattern.compile("(\\w+): (\\d+)"); + Matcher keyValueMatcher = keyValuePattern.matcher(status); + + while (keyValueMatcher.find()) { + String key = keyValueMatcher.group(1); + String value = keyValueMatcher.group(2); + + System.out.println(key + ": " + value); + } + + // Use regular expressions to match and extract the keys and corresponding values for "accepts", "handled", and "requests". + Pattern valuesPattern = Pattern.compile("server\\s+(\\w+)\\s+(\\w+)\\s+(\\w+)"); + Matcher valuesMatcher = valuesPattern.matcher(status); + + if (valuesMatcher.find()) { + String accepts = valuesMatcher.group(1); + String handled = valuesMatcher.group(2); + String requests = valuesMatcher.group(3); + + System.out.println("accepts: " + accepts); + System.out.println("handled: " + handled); + System.out.println("requests: " + requests); + } + + Pattern valuePattern = Pattern.compile("(\\d+) (\\d+) (\\d+)"); + Matcher valueMatcher = valuePattern.matcher(status); + if (valueMatcher.find()) { + int accepts = Integer.parseInt(valueMatcher.group(1)); + int handled = Integer.parseInt(valueMatcher.group(2)); + int requests = Integer.parseInt(valueMatcher.group(3)); + System.out.println("accepts: " + accepts); + System.out.println("handled: " + handled); + System.out.println("requests: " + requests); + } + } + + @Test + public void testReqStatusMatch() { + String urlContent = """ + zone_name\tkey\tmax_active\tmax_bw\ttraffic\trequests\tactive\tbandwidth + server_addr\t172.17.0.3\t2\t 440\t68K\t23\t1\t 0 + server_name\tlocalhost\t2\t 440\t68K\t23\t1\t 0 + server_url\tlocalhost/\t1\t 0\t 0\t4\t0\t 0 + server_url\tlocalhost/index.html\t1\t 104\t27K\t4\t0\t 0 + server_url\tlocalhost/nginx-status\t1\t 32\t 9896\t5\t0\t 0 + server_url\tlocalhost/req-status\t1\t 0\t31K\t10\t1\t 0"""; + + String[] lines = urlContent.split("\\r?\\n"); + List<String> zoneNames = new ArrayList<>(); + List<String> keys = new ArrayList<>(); + List<String> maxActives = new ArrayList<>(); + List<String> maxBws = new ArrayList<>(); + List<String> traffics = new ArrayList<>(); + List<String> requests = new ArrayList<>(); + List<String> actives = new ArrayList<>(); + List<String> bandwidths = new ArrayList<>(); + + for (int i = 1; i < lines.length; i++) { + String[] values = lines[i].split("\\s+"); + zoneNames.add(values[0]); + keys.add(values[1]); + maxActives.add(values[2]); + maxBws.add(values[3]); + traffics.add(values[4]); + requests.add(values[5]); + actives.add(values[6]); + bandwidths.add(values[7]); + } + + System.out.println("zone_name: " + zoneNames); + System.out.println("key: " + keys); + System.out.println("max_active: " + maxActives); + System.out.println("max_bw: " + maxBws); + System.out.println("traffic: " + traffics); + System.out.println("requests: " + requests); + System.out.println("active: " + actives); + System.out.println("bandwidth: " + bandwidths); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ntp/NtpCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ntp/NtpCollectImplTest.java new file mode 100644 index 0000000..4509e32 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ntp/NtpCollectImplTest.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ntp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.net.ntp.NTPUDPClient; +import org.apache.commons.net.ntp.NtpV3Impl; +import org.apache.commons.net.ntp.NtpV3Packet; +import org.apache.commons.net.ntp.TimeInfo; +import org.apache.commons.net.ntp.TimeStamp; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.NtpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link NtpCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +class NtpCollectImplTest { + + @InjectMocks + private NtpCollectImpl ntpCollect; + + @Test + void testCollect() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + NtpProtocol telnetProtocol = NtpProtocol.builder() + .host("192.168.77.100") + .port("1234") + .timeout("10") + .build(); + + NtpV3Packet packet = new NtpV3Impl(); + int version = 1; + packet.setVersion(version); + packet.setOriginateTimeStamp(new TimeStamp(3000)); + packet.setReceiveTimeStamp(new TimeStamp(2000)); + packet.setTransmitTime(new TimeStamp(1000)); + TimeInfo timeInfo = new TimeInfo(packet, 1000, false); + + MockedConstruction<NTPUDPClient> mocked = + Mockito.mockConstruction(NTPUDPClient.class, (client, context) -> { + Mockito.doNothing().when(client).open(); + Mockito.when(client.getTime(InetAddress.getByName(telnetProtocol.getHost()))).thenReturn(timeInfo); + + }); + + List<String> aliasField = new ArrayList<>(); + aliasField.add("responseTime"); + aliasField.add("version"); + Metrics metrics = new Metrics(); + metrics.setNtp(telnetProtocol); + metrics.setAliasFields(aliasField); + ntpCollect.preCheck(metrics); + ntpCollect.collect(builder, metrics); + assertEquals(builder.getValuesCount(), 1); + for (CollectRep.ValueRow valueRow : builder.getValuesList()) { + assertNotNull(valueRow.getColumns(0)); + assertEquals(valueRow.getColumns(1), String.valueOf(version)); + } + + mocked.close(); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/plc/PlcCollectTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/plc/PlcCollectTest.java new file mode 100644 index 0000000..13dadaf --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/plc/PlcCollectTest.java @@ -0,0 +1,253 @@ +/* + * 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.collector.collect.plc; + + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.PlcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.plc4x.java.api.PlcConnection; +import org.apache.plc4x.java.api.messages.PlcReadRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test case for {@link AbstractPlcCollectImpl} + */ +public class PlcCollectTest { + private AbstractPlcCollectImpl plcCollect; + private Metrics metrics; + private CollectRep.MetricsData.Builder builder; + + @BeforeEach + public void setup() { + plcCollect = new AbstractPlcCollectImpl() { + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_PLC; + } + + @Override + protected String getConnectionString(Metrics metrics) { + PlcProtocol plcProtocol = metrics.getPlc(); + return "modbus-tcp:tcp://" + plcProtocol.getHost() + ":" + plcProtocol.getPort() + "?unit-identifier=" + plcProtocol.getSlaveId(); + } + + @Override + protected PlcReadRequest buildRequest(Metrics metrics, PlcConnection connection) { + PlcProtocol modbus = metrics.getPlc(); + List<String> registerAddressList = modbus.getRegisterAddresses(); + // Create a new read request: + PlcReadRequest.Builder requestBuilder = connection.readRequestBuilder(); + for (int i = 0; i < registerAddressList.size(); i++) { + String s1 = modbus.getAddressSyntax() + ":" + registerAddressList.get(i); + requestBuilder.addTagAddress(metrics.getPlc().getAddressSyntax() + ":" + i, s1); + } + return requestBuilder.build(); + } + }; + PlcProtocol plc = PlcProtocol.builder().build(); + metrics = Metrics.builder() + .plc(plc) + .build(); + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void preCheck() { + // host is empty + assertThrows(IllegalArgumentException.class, () -> { + plcCollect.preCheck(metrics); + }); + + // port is empty default add 502 + assertThrows(IllegalArgumentException.class, () -> { + PlcProtocol plc = PlcProtocol.builder().build(); + plc.setHost("127.0.0.1"); + metrics.setPlc(plc); + plcCollect.preCheck(metrics); + }); + + // driverName version is empty + assertThrows(IllegalArgumentException.class, () -> { + PlcProtocol plc = PlcProtocol.builder().build(); + plc.setHost("127.0.0.1"); + plc.setPort("502"); + metrics.setPlc(plc); + plcCollect.preCheck(metrics); + }); + + // addressSyntax is empty + assertThrows(IllegalArgumentException.class, () -> { + PlcProtocol plc = PlcProtocol.builder().build(); + plc.setHost("127.0.0.1"); + plc.setPort("502"); + plc.setDriverName("modbus-tcp"); + metrics.setPlc(plc); + plcCollect.preCheck(metrics); + }); + + // registerAddresses version is empty + assertThrows(IllegalArgumentException.class, () -> { + PlcProtocol plc = PlcProtocol.builder().build(); + plc.setHost("127.0.0.1"); + plc.setPort("502"); + plc.setDriverName("modbus-tcp"); + plc.setAddressSyntax("holding-register"); + metrics.setPlc(plc); + plcCollect.preCheck(metrics); + }); + + // aliasFields is empty + assertThrows(IllegalArgumentException.class, () -> { + PlcProtocol plc = PlcProtocol.builder().build(); + plc.setHost("127.0.0.1"); + plc.setPort("502"); + plc.setDriverName("modbus-tcp"); + plc.setAddressSyntax("holding-register"); + plc.setRegisterAddresses(List.of("1")); + metrics.setPlc(plc); + plcCollect.preCheck(metrics); + }); + + // the number of aliasFields is not equal to registerAddresses(responseTime doesn't count) + assertThrows(IllegalArgumentException.class, () -> { + PlcProtocol plc = PlcProtocol.builder().build(); + plc.setHost("127.0.0.1"); + plc.setPort("502"); + plc.setDriverName("modbus-tcp"); + plc.setAddressSyntax("holding-register"); + plc.setRegisterAddresses(List.of("1", "2")); + metrics.setAliasFields(List.of( + "responseTime", + "holding-register:0" + )); + metrics.setPlc(plc); + plcCollect.preCheck(metrics); + }); + + // everything is ok + assertDoesNotThrow(() -> { + PlcProtocol plc = PlcProtocol.builder().build(); + plc.setHost("127.0.0.1"); + plc.setPort("502"); + plc.setDriverName("modbus-tcp"); + plc.setAddressSyntax("holding-register"); + plc.setRegisterAddresses(List.of("1", "2", "3[2]")); + plc.setSlaveId("2"); + plc.setTimeout("500"); + metrics.setPlc(plc); + metrics.setAliasFields(List.of( + "responseTime", + "holding-register:0", + "holding-register:1", + "holding-register:2-0", + "holding-register:2-1" + )); + plcCollect.preCheck(metrics); + }); + } + + @Test + void supportProtocol() { + Assertions.assertEquals(DispatchConstants.PROTOCOL_PLC, plcCollect.supportProtocol()); + } + + @Test + void collect() { + // with holding-register + assertDoesNotThrow(() -> { + PlcProtocol plc = PlcProtocol.builder().build(); + plc.setHost("127.0.0.1"); + plc.setPort("502"); + plc.setDriverName("modbus-tcp"); + plc.setAddressSyntax("holding-register"); + plc.setRegisterAddresses(List.of("1", "2[3]")); + plc.setSlaveId("1"); + plc.setTimeout("500"); + + metrics.setPlc(plc); + metrics.setAliasFields(List.of( + "responseTime", + "holding-register:0", + "holding-register:1-0", + "holding-register:1-1", + "holding-register:1-2" + )); + plcCollect.preCheck(metrics); + plcCollect.collect(builder, metrics); + }); + + // with coil + assertDoesNotThrow(() -> { + PlcProtocol plc = PlcProtocol.builder().build(); + plc.setHost("127.0.0.1"); + plc.setPort("502"); + plc.setDriverName("modbus-tcp"); + plc.setAddressSyntax("coil"); + plc.setRegisterAddresses(List.of("1", "2[3]")); + plc.setSlaveId("1"); + plc.setTimeout("500"); + + metrics.setPlc(plc); + metrics.setAliasFields(List.of( + "responseTime", + "coil:0", + "coil:1-0", + "coil:1-1", + "coil:1-2" + )); + plcCollect.preCheck(metrics); + plcCollect.collect(builder, metrics); + }); + + + // with slaveId2 + // with holding-register + assertDoesNotThrow(() -> { + PlcProtocol plc = PlcProtocol.builder().build(); + plc.setHost("127.0.0.1"); + plc.setPort("502"); + plc.setDriverName("modbus-tcp"); + plc.setAddressSyntax("holding-register"); + plc.setRegisterAddresses(List.of("1", "2[3]")); + plc.setSlaveId("2"); + plc.setTimeout("500"); + + metrics.setPlc(plc); + metrics.setAliasFields(List.of( + "responseTime", + "holding-register:0", + "holding-register:1-0", + "holding-register:1-1", + "holding-register:1-2" + )); + plcCollect.preCheck(metrics); + plcCollect.collect(builder, metrics); + }); + + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/pop3/Pop3CollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/pop3/Pop3CollectImplTest.java new file mode 100644 index 0000000..83cdaf9 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/pop3/Pop3CollectImplTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.pop3; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.Pop3Protocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link Pop3CollectImpl} + */ +public class Pop3CollectImplTest { + private Pop3CollectImpl pop3Collect; + + @BeforeEach + void setUp() throws Exception { + pop3Collect = new Pop3CollectImpl(); + } + + @Test + void preCheck() throws Exception { + // metrics is null + assertThrows(IllegalArgumentException.class, () -> { + pop3Collect.preCheck(null); + }); + + // protocol is null + assertThrows(IllegalArgumentException.class, () -> { + Metrics metrics = Metrics.builder().build(); + pop3Collect.preCheck(metrics); + }); + + // protocol is invalid + assertThrows(IllegalArgumentException.class, () -> { + Pop3Protocol pop3 = Pop3Protocol.builder().build(); + Metrics metrics = Metrics.builder().pop3(pop3).build(); + pop3Collect.preCheck(metrics); + }); + + // everything is ok + assertDoesNotThrow(() -> { + Pop3Protocol pop3 = Pop3Protocol.builder() + .host("localhost") + .port("110") + .timeout("1") + .ssl("true") + .email("test@example.com") + .authorize("auth") + .build(); + Metrics metrics = Metrics.builder().pop3(pop3).build(); + pop3Collect.preCheck(metrics); + }); + } + + @Test + void collect() throws Exception { + Pop3Protocol pop3 = Pop3Protocol.builder() + .host("localhost") + .port("110") + .timeout("1") + .ssl("true") + .email("test@example.com") + .authorize("auth") + .build(); + Metrics metrics = Metrics.builder().pop3(pop3).build(); + + assertDoesNotThrow(() -> { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + pop3Collect.collect(builder, metrics); + }); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_POP3, pop3Collect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserSingleTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserSingleTest.java new file mode 100644 index 0000000..a563ffa --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserSingleTest.java @@ -0,0 +1,601 @@ +/* + * 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.collector.collect.prometheus.parser; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * Parse a Single metric in prometheus test + */ +public class OnlineParserSingleTest { + + @Disabled // Disabled due to the fact that the URL is not reachable unless you have the Prometheus server running + @Test + void parseMetrics() throws Exception { + URL url = new URL("http://localhost:9090/metrics"); + InputStream inputStream = url.openStream(); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "go_gc_duration_seconds"); + assertNotNull(metricFamilyMap); + } + + @Disabled // because unless you have already saved the locally tested files + @Test + void parseTestFile() throws Exception { + InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("test_file.txt"); + assertNotNull(inputStream, "Failed to load test_file.txt resource"); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "windows_textfile_scrape_error"); + assertNotNull(metricFamilyMap); + } + + + @Test + void parseMetricsOutOfOrder() throws Exception { + String str = """ + # HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles. + # TYPE go_gc_duration_seconds summary + jvm_gc_pause_seconds_count{action="end of major GC",cause="Metadata GC Threshold",} 1.0 + jvm_gc_pause_seconds_sum{action="end of major GC",cause="Metadata GC Threshold",} 0.139 + jvm_gc_pause_seconds_count{action="end of minor GC",cause="Metadata GC Threshold",} 1.0 + jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Metadata GC Threshold",} 0.02 + jvm_gc_pause_seconds_count{action="end of minor GC",cause="Allocation Failure",} 5.0 + jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Allocation Failure",} 0.082 + go_gc_duration_seconds{quantile="0"} 2.0209e-05 + go_gc_duration_seconds{quantile="0"} 2.0209e-05 + go_gc_duration_seconds{quantile="0.25"} 6.6917e-05 + go_gc_duration_seconds{quantile="0.5"} -Inf + go_gc_duration_seconds{quantile="0.75"} +Inf + go_gc_duration_seconds{quantile="1"} NaN + go_gc_duration_seconds_sum 0.001134793 + go_gc_duration_seconds_count 5 + # HELP go_goroutines Number of goroutines that currently exist. + # TYPE go_goroutines gauge + go_goroutines 32 + # HELP go_info Information about the Go environment. + # TYPE go_info gauge + go_info{version="go1.21.6"} 1 + # HELP go_memstats_alloc_bytes Number of bytes allocated and still in use. + # TYPE go_memstats_alloc_bytes gauge + go_memstats_alloc_bytes 1.5716224e+07 + # HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed. + # TYPE go_memstats_alloc_bytes_total counter + go_memstats_alloc_bytes_total 2.0707544e+07 + # HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table. + # TYPE go_memstats_buck_hash_sys_bytes gauge + go_memstats_buck_hash_sys_bytes 1.457881e+06 + # HELP go_memstats_frees_total Total number of frees. + # TYPE go_memstats_frees_total counter + go_memstats_frees_total 50438 + # HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata. + # TYPE go_memstats_gc_sys_bytes gauge + go_memstats_gc_sys_bytes 4.614808e+06"""; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap1 = parseMetrics(inputStream, "go_memstats_buck_hash_sys_bytes"); + assertNotNull(metricFamilyMap1); + MetricFamily metricFamily = metricFamilyMap1.get("go_memstats_buck_hash_sys_bytes"); + assertNotNull(metricFamily); + assertNotNull(metricFamily.getName()); + assertEquals("go_memstats_buck_hash_sys_bytes", metricFamily.getName()); + assertEquals(1, metricFamily.getMetricList().size()); + assertEquals(1457881.0, metricFamily.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithCrLf() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\r\n" + + "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\r\n" + + "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139\r\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count"); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\r\n" + + "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0 1234567890\r\n" + + "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139 1234567890\r\n"; + inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum"); + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily.getName()); + assertEquals(0.139, metricFamily.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithLf() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\n" + + "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count"); + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0 1234567890\n" + + "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139 1234567890\n"; + inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum"); + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily.getName()); + assertEquals(0.139, metricFamily.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithoutFinalNewline() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\r\n" + + "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\r\n" + + "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139"; + + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count"); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\n" + + "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139"; + + inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum"); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily.getName()); + assertEquals(0.139, metricFamily.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithEmptyLabelsAndCrLf() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\r\n" + + "jvm_gc_pause_seconds_count 1.0\r\n" + + "jvm_gc_pause_seconds_sum{} 0.139"; + + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count"); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum"); + assertNotNull(metricFamilyMap); + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithEmptyLabelsAndLf() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "jvm_gc_pause_seconds_count 1.0\n" + + "jvm_gc_pause_seconds_sum{} 0.139"; + + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count"); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum"); + assertNotNull(metricFamilyMap); + + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithMixedLineEndings() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "jvm_gc_pause_seconds_count 1.0\n" + + "jvm_gc_pause_seconds_sum{} 0.139\r\n"; + + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count"); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum"); + assertNotNull(metricFamilyMap); + + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithLfCrLineEnding() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n\r" + + "# TYPE go_gc_duration_seconds summary\n\r" + + "jvm_gc_pause_seconds_count 1.0\n\r" + + "jvm_gc_pause_seconds_sum{} 0.139\n\r"; + + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count"); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum"); + assertNotNull(metricFamilyMap); + + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithMixedLineEndingsIncludingLfCr() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "jvm_gc_pause_seconds_count 1.0\n\r" + + "jvm_gc_pause_seconds_sum{} 0.139\r\n" + + "jvm_gc_pause_seconds_max{} 0.139\n"; + + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_count"); + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum"); + assertNotNull(metricFamilyMap); + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + + inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_max"); + assertNotNull(metricFamilyMap); + MetricFamily metricFamily2 = metricFamilyMap.get("jvm_gc_pause_seconds_max"); + assertEquals("jvm_gc_pause_seconds_max", metricFamily2.getName()); + assertEquals(0.139, metricFamily2.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithOnlyLfCr() throws Exception { + String str = "jvm_gc_pause_seconds_sum 42.0\n\r"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "jvm_gc_pause_seconds_sum"); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily.getName()); + assertEquals(42.0, metricFamily.getMetricList().get(0).getValue()); + } + + + @Test + void testParseMetricEscape1() throws Exception { + // test escape '\\' + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "windows_service_info{display_name=\"\\\\Application Layer \nGateway Service\",name=\"alg\\\\\",process_id=\"0\",run_as=\"NT AUTHORITY\\\\LocalService\"} 1\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "windows_service_info"); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("windows_service_info"); + assertEquals("windows_service_info", metricFamily.getName()); + assertNotNull(metricFamily.getMetricList().get(0).getLabels()); + assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName()); + assertEquals("\\Application Layer \nGateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue()); + + assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName()); + assertEquals("alg\\", metricFamily.getMetricList().get(0).getLabels().get(1).getValue()); + + assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName()); + assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue()); + + assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName()); + assertEquals("NT AUTHORITY\\LocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue()); + } + + @Test + void testParseMetricEscape2() throws Exception { + // test escape '\"' + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "windows_service_info{display_name=\"\\\"Application Layer \\\"Gateway Service\",name=\"alg\\\"\",process_id=\"0\",run_as=\"NT AUTHORITY\\\"LocalService\"} 1\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "windows_service_info"); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("windows_service_info"); + assertEquals("windows_service_info", metricFamily.getName()); + assertNotNull(metricFamily.getMetricList().get(0).getLabels()); + assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName()); + assertEquals("\"Application Layer \"Gateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue()); + + assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName()); + assertEquals("alg\"", metricFamily.getMetricList().get(0).getLabels().get(1).getValue()); + + assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName()); + assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue()); + + assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName()); + assertEquals("NT AUTHORITY\"LocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue()); + + + } + + @Test + void testParseMetricEscape3() throws Exception { + // test escape '\n' + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "windows_service_info{display_name=\"\nApplication Layer \nGateway Service\",name=\"alg\n\",process_id=\"0\",run_as=\"NT AUTHORITY\nLocalService\"} 1\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "windows_service_info"); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("windows_service_info"); + assertEquals("windows_service_info", metricFamily.getName()); + assertNotNull(metricFamily.getMetricList().get(0).getLabels()); + assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName()); + assertEquals("\nApplication Layer \nGateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue()); + + assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName()); + assertEquals("alg\n", metricFamily.getMetricList().get(0).getLabels().get(1).getValue()); + + assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName()); + assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue()); + + assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName()); + assertEquals("NT AUTHORITY\nLocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue()); + } + + @Test + void testParseMetricsWithChineseLabels() throws Exception { + String str = "ST22{Dump_Name=\"Dump总数\",HostName=\"SAP_DEV\",instance_hostname=\"sapdev\"} 2\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "ST22"); + + assertNotNull(metricFamilyMap); + MetricFamily metricFamily = metricFamilyMap.get("ST22"); + assertNotNull(metricFamily); + assertEquals("ST22", metricFamily.getName()); + assertEquals(1, metricFamily.getMetricList().size()); + assertEquals(2.0, metricFamily.getMetricList().get(0).getValue()); + + // Verify Chinese label value is correctly parsed + MetricFamily.Label dumpNameLabel = metricFamily.getMetricList().get(0).getLabels().stream() + .filter(label -> "Dump_Name".equals(label.getName())) + .findFirst().orElse(null); + assertNotNull(dumpNameLabel); + assertEquals("Dump总数", dumpNameLabel.getValue()); + } + + private Map<String, MetricFamily> parseMetrics(InputStream inputStream, String metric) throws IOException { + return OnlineParser.parseMetrics(inputStream, metric); + } + + @Test + void testHandleUtf8Character_ValidTwoByteSequence() throws Exception { + // Test valid 2-byte UTF-8 sequence: é (U+00E9) = 0xC3 0xA9 + String str = "test_metric{label=\"caf\u00e9\"} 1\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric"); + + assertNotNull(metricFamilyMap); + MetricFamily metricFamily = metricFamilyMap.get("test_metric"); + assertNotNull(metricFamily); + assertEquals(1, metricFamily.getMetricList().size()); + + MetricFamily.Label label = metricFamily.getMetricList().get(0).getLabels().get(0); + assertEquals("label", label.getName()); + assertEquals("café", label.getValue()); + } + + @Test + void testHandleUtf8Character_ValidThreeByteSequence() throws Exception { + // Test valid 3-byte UTF-8 sequence: Chinese character (U+4E2D) = 0xE4 0xB8 0xAD + String str = "test_metric{label=\"\u4e2d\u6587\"} 1\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric"); + + assertNotNull(metricFamilyMap); + MetricFamily metricFamily = metricFamilyMap.get("test_metric"); + assertNotNull(metricFamily); + assertEquals(1, metricFamily.getMetricList().size()); + + MetricFamily.Label label = metricFamily.getMetricList().get(0).getLabels().get(0); + assertEquals("label", label.getName()); + assertEquals("\u4e2d\u6587", label.getValue()); + } + + @Test + void testHandleUtf8Character_ValidFourByteSequence() throws Exception { + // Test valid 4-byte UTF-8 sequence: 𝕳 (U+1D573) = 0xF0 0x9D 0x95 0xB3 + String str = "test_metric{label=\"\uD835\uDD73ello\"} 1\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric"); + + assertNotNull(metricFamilyMap); + MetricFamily metricFamily = metricFamilyMap.get("test_metric"); + assertNotNull(metricFamily); + assertEquals(1, metricFamily.getMetricList().size()); + + MetricFamily.Label label = metricFamily.getMetricList().get(0).getLabels().get(0); + assertEquals("label", label.getName()); + assertEquals("𝕳ello", label.getValue()); + } + + @Test + void testHandleUtf8Character_InvalidFirstByte_0xC0() throws Exception { + // Test invalid first byte 0xC0 (overlong encoding) - should cause parsing failure + byte[] invalidBytes = { + 't', 'e', 's', 't', '_', 'm', 'e', 't', 'r', 'i', 'c', '{', 'l', 'a', 'b', 'e', 'l', '=', '"', + (byte) 0xC0, (byte) 0x80, + '"', '}', ' ', '1', '\n' + }; + InputStream inputStream = new ByteArrayInputStream(invalidBytes); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric"); + assertNull(metricFamilyMap); + } + + @Test + void testHandleUtf8Character_InvalidFirstByte_0xC1() throws Exception { + // Test invalid first byte 0xC1 (overlong encoding) - should cause parsing failure + byte[] invalidBytes = { + 't', 'e', 's', 't', '_', 'm', 'e', 't', 'r', 'i', 'c', '{', 'l', 'a', 'b', 'e', 'l', '=', '"', + (byte) 0xC1, (byte) 0x80, + '"', '}', ' ', '1', '\n' + }; + InputStream inputStream = new ByteArrayInputStream(invalidBytes); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric"); + assertNull(metricFamilyMap); + } + + @Test + void testHandleUtf8Character_InvalidFirstByte_0xF5() throws Exception { + // Test invalid first byte 0xF5 (out of range) - should cause parsing failure + byte[] invalidBytes = { + 't', 'e', 's', 't', '_', 'm', 'e', 't', 'r', 'i', 'c', '{', 'l', 'a', 'b', 'e', 'l', '=', '"', + (byte) 0xF5, (byte) 0x80, (byte) 0x80, (byte) 0x80, + '"', '}', ' ', '1', '\n' + }; + InputStream inputStream = new ByteArrayInputStream(invalidBytes); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric"); + assertNull(metricFamilyMap); + } + + @Test + void testHandleUtf8Character_InvalidContinuationByte() throws Exception { + // Test invalid continuation byte (should be 10xxxxxx but is 11xxxxxx) - should cause parsing failure + byte[] invalidBytes = { + 't', 'e', 's', 't', '_', 'm', 'e', 't', 'r', 'i', 'c', '{', 'l', 'a', 'b', 'e', 'l', '=', '"', + (byte) 0xC3, (byte) 0xC0, + '"', '}', ' ', '1', '\n' + }; + InputStream inputStream = new ByteArrayInputStream(invalidBytes); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric"); + assertNull(metricFamilyMap); + } + + @Test + void testHandleUtf8Character_MixedValidInvalid() throws Exception { + // Test mixed valid UTF-8 sequences (removed invalid sequences that cause parsing to fail) + byte[] mixedBytes = { + 't', 'e', 's', 't', '_', 'm', 'e', 't', 'r', 'i', 'c', '{', 'l', 'a', 'b', 'e', 'l', '=', '"', + 'H', 'e', 'l', 'l', 'o', + (byte) 0xC3, (byte) 0xA9, + (byte) 0xE4, (byte) 0xB8, (byte) 0xAD, + (byte) 0xF0, (byte) 0x9F, (byte) 0x98, (byte) 0x80, + '"', '}', ' ', '1', '\n' + }; + InputStream inputStream = new ByteArrayInputStream(mixedBytes); + Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric"); + + assertNotNull(metricFamilyMap); + MetricFamily metricFamily = metricFamilyMap.get("test_metric"); + assertNotNull(metricFamily); + assertEquals(1, metricFamily.getMetricList().size()); + + MetricFamily.Label label = metricFamily.getMetricList().get(0).getLabels().get(0); + assertEquals("label", label.getName()); + String value = label.getValue(); + assertNotNull(value); + assertEquals("Hello\u00e9\u4e2d\ud83d\ude00", value); + } +} \ No newline at end of file diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserTest.java new file mode 100644 index 0000000..4f515b8 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserTest.java @@ -0,0 +1,462 @@ +/* + * 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.collector.collect.prometheus.parser; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +class OnlineParserTest { + + @Disabled // Disabled due to the fact that the URL is not reachable unless you have the Prometheus server running + @Test + void parseMetrics() throws Exception { + URL url = new URL("http://localhost:9090/metrics"); + InputStream inputStream = url.openStream(); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + assertNotNull(metricFamilyMap); + } + + @Disabled // because unless you have already saved the locally tested files + @Test + void parseTestFile() throws Exception { + InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("test_file.txt"); + assertNotNull(inputStream, "Failed to load test_file.txt resource"); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + assertNotNull(metricFamilyMap); + } + + + @Test + void parseMetrics2() throws Exception { + String str = """ + # HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles. + # TYPE go_gc_duration_seconds summary + jvm_gc_pause_seconds_count{action="end of major GC",cause="Metadata GC Threshold",} 1.0 + jvm_gc_pause_seconds_sum{action="end of major GC",cause="Metadata GC Threshold",} 0.139 + jvm_gc_pause_seconds_count{action="end of minor GC",cause="Metadata GC Threshold",} 1.0 + jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Metadata GC Threshold",} 0.02 + jvm_gc_pause_seconds_count{action="end of minor GC",cause="Allocation Failure",} 5.0 + jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Allocation Failure",} 0.082 + go_gc_duration_seconds{quantile="0"} 2.0209e-05 + go_gc_duration_seconds{quantile="0"} 2.0209e-05 + go_gc_duration_seconds{quantile="0.25"} 6.6917e-05 + go_gc_duration_seconds{quantile="0.5"} -Inf + go_gc_duration_seconds{quantile="0.75"} +Inf + go_gc_duration_seconds{quantile="1"} NaN + go_gc_duration_seconds_sum 0.001134793 + go_gc_duration_seconds_count 5 + # HELP go_goroutines Number of goroutines that currently exist. + # TYPE go_goroutines gauge + go_goroutines 32 + # HELP go_info Information about the Go environment. + # TYPE go_info gauge + go_info{version="go1.21.6"} 1 + # HELP go_memstats_alloc_bytes Number of bytes allocated and still in use. + # TYPE go_memstats_alloc_bytes gauge + go_memstats_alloc_bytes 1.5716224e+07 + # HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed. + # TYPE go_memstats_alloc_bytes_total counter + go_memstats_alloc_bytes_total 2.0707544e+07 + # HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table. + # TYPE go_memstats_buck_hash_sys_bytes gauge + go_memstats_buck_hash_sys_bytes 1.457881e+06 + # HELP go_memstats_frees_total Total number of frees. + # TYPE go_memstats_frees_total counter + go_memstats_frees_total 50438 + # HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata. + # TYPE go_memstats_gc_sys_bytes gauge + go_memstats_gc_sys_bytes 4.614808e+06"""; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap1 = OnlineParser.parseMetrics(inputStream); + Map<String, MetricFamily> metricFamilyMap2 = TextParser.textToMetricFamilies(str); + assertNotNull(metricFamilyMap1); + assertNotNull(metricFamilyMap2); + assertEquals(metricFamilyMap1.size(), metricFamilyMap2.size()); + metricFamilyMap2.forEach((metricFamilyName, metricFamily2) -> { + if (!metricFamilyMap1.containsKey(metricFamilyName)) { + fail("parse failed, different result from two parser."); + } + MetricFamily metricFamily1 = metricFamilyMap1.get(metricFamilyName); + Set<Double> metricValueSet = metricFamily2.getMetricList().stream().map(MetricFamily.Metric::getValue).collect(Collectors.toSet()); + metricFamily1.getMetricList().forEach(metric -> { + // this is for something different between two algorithms above, and both of them is current on this parsing behavior. + if (!(metric.getValue() == Double.POSITIVE_INFINITY || metric.getValue() == Double.NEGATIVE_INFINITY)) { + if (!metricValueSet.contains(metric.getValue())) { + fail(); + } + } + }); + }); + } + + @Test + void testParseMetricsWithCrLf() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\r\n" + + "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\r\n" + + "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139\r\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + assertNotNull(metricFamilyMap); + assertEquals(2, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + + str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\r\n" + + "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0 1234567890\r\n" + + "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139 1234567890\r\n"; + inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + metricFamilyMap = OnlineParser.parseMetrics(inputStream); + assertNotNull(metricFamilyMap); + assertEquals(2, metricFamilyMap.values().size()); + + metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithLf() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\n" + + "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + assertNotNull(metricFamilyMap); + assertEquals(2, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + + str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0 1234567890\n" + + "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139 1234567890\n"; + inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + metricFamilyMap = OnlineParser.parseMetrics(inputStream); + assertNotNull(metricFamilyMap); + assertEquals(2, metricFamilyMap.values().size()); + + metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithoutFinalNewline() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\r\n" + + "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\r\n" + + "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139"; + + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + + assertNotNull(metricFamilyMap); + assertEquals(2, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + + str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\n" + + "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139"; + + inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + metricFamilyMap = OnlineParser.parseMetrics(inputStream); + + assertNotNull(metricFamilyMap); + assertEquals(2, metricFamilyMap.values().size()); + + metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithEmptyLabelsAndCrLf() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\r\n" + + "jvm_gc_pause_seconds_count 1.0\r\n" + + "jvm_gc_pause_seconds_sum{} 0.139"; + + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + + assertNotNull(metricFamilyMap); + assertEquals(2, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + + } + + @Test + void testParseMetricsWithEmptyLabelsAndLf() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "jvm_gc_pause_seconds_count 1.0\n" + + "jvm_gc_pause_seconds_sum{} 0.139"; + + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + + assertNotNull(metricFamilyMap); + assertEquals(2, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithMixedLineEndings() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "jvm_gc_pause_seconds_count 1.0\n" + + "jvm_gc_pause_seconds_sum{} 0.139\r\n"; + + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + + assertNotNull(metricFamilyMap); + assertEquals(2, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithLfCrLineEnding() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n\r" + + "# TYPE go_gc_duration_seconds summary\n\r" + + "jvm_gc_pause_seconds_count 1.0\n\r" + + "jvm_gc_pause_seconds_sum{} 0.139\n\r"; + + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + + assertNotNull(metricFamilyMap); + assertEquals(2, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithMixedLineEndingsIncludingLfCr() throws Exception { + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "jvm_gc_pause_seconds_count 1.0\n\r" + + "jvm_gc_pause_seconds_sum{} 0.139\r\n" + + "jvm_gc_pause_seconds_max{} 0.139\n"; + + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + + assertNotNull(metricFamilyMap); + assertEquals(3, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count"); + assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName()); + assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue()); + + MetricFamily metricFamily2 = metricFamilyMap.get("jvm_gc_pause_seconds_max"); + assertEquals("jvm_gc_pause_seconds_max", metricFamily2.getName()); + assertEquals(0.139, metricFamily2.getMetricList().get(0).getValue()); + } + + @Test + void testParseMetricsWithOnlyLfCr() throws Exception { + String str = "jvm_gc_pause_seconds_sum 42.0\n\r"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_sum"); + assertEquals("jvm_gc_pause_seconds_sum", metricFamily.getName()); + assertEquals(42.0, metricFamily.getMetricList().get(0).getValue()); + } + + + @Test + void testParseMetricEscape1() throws Exception { + // test escape '\\' + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "windows_service_info{display_name=\"\\\\Application Layer \nGateway Service\",name=\"alg\\\\\",process_id=\"0\",run_as=\"NT AUTHORITY\\\\LocalService\"} 1\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("windows_service_info"); + assertEquals("windows_service_info", metricFamily.getName()); + assertNotNull(metricFamily.getMetricList().get(0).getLabels()); + assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName()); + assertEquals("\\Application Layer \nGateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue()); + + assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName()); + assertEquals("alg\\", metricFamily.getMetricList().get(0).getLabels().get(1).getValue()); + + assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName()); + assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue()); + + assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName()); + assertEquals("NT AUTHORITY\\LocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue()); + } + + @Test + void testParseMetricEscape2() throws Exception { + // test escape '\"' + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "windows_service_info{display_name=\"\\\"Application Layer \\\"Gateway Service\",name=\"alg\\\"\",process_id=\"0\",run_as=\"NT AUTHORITY\\\"LocalService\"} 1\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("windows_service_info"); + assertEquals("windows_service_info", metricFamily.getName()); + assertNotNull(metricFamily.getMetricList().get(0).getLabels()); + assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName()); + assertEquals("\"Application Layer \"Gateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue()); + + assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName()); + assertEquals("alg\"", metricFamily.getMetricList().get(0).getLabels().get(1).getValue()); + + assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName()); + assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue()); + + assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName()); + assertEquals("NT AUTHORITY\"LocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue()); + + + } + + @Test + void testParseMetricEscape3() throws Exception { + // test escape '\n' + String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n" + + "# TYPE go_gc_duration_seconds summary\n" + + "windows_service_info{display_name=\"\nApplication Layer \nGateway Service\",name=\"alg\n\",process_id=\"0\",run_as=\"NT AUTHORITY\nLocalService\"} 1\n"; + InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + + Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream); + + assertNotNull(metricFamilyMap); + assertEquals(1, metricFamilyMap.values().size()); + + MetricFamily metricFamily = metricFamilyMap.get("windows_service_info"); + assertEquals("windows_service_info", metricFamily.getName()); + assertNotNull(metricFamily.getMetricList().get(0).getLabels()); + assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size()); + assertEquals(1.0, metricFamily.getMetricList().get(0).getValue()); + + assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName()); + assertEquals("\nApplication Layer \nGateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue()); + + assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName()); + assertEquals("alg\n", metricFamily.getMetricList().get(0).getLabels().get(1).getValue()); + + assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName()); + assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue()); + + assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName()); + assertEquals("NT AUTHORITY\nLocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/push/PushCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/push/PushCollectImplTest.java new file mode 100644 index 0000000..ae70a96 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/push/PushCollectImplTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.push; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.PushProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link PushCollectImpl} + */ +public class PushCollectImplTest { + private PushCollectImpl pushCollect; + private PushProtocol push; + private CollectRep.MetricsData.Builder builder; + + @BeforeEach + public void setup() { + pushCollect = new PushCollectImpl(); + push = PushProtocol.builder().uri("/metrics").host("example.com").port("60").build(); + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void preCheck() throws Exception { + // metrics is null + assertThrows(IllegalArgumentException.class, () -> pushCollect.preCheck(null)); + + // protocol is null + assertThrows(IllegalArgumentException.class, () -> pushCollect.preCheck(new Metrics())); + + // everyting is ok + assertDoesNotThrow(() -> { + pushCollect.preCheck(Metrics.builder().push(push).build()); + }); + } + + @Test + void collect() throws Exception { + assertDoesNotThrow(() -> { + pushCollect.collect(builder, Metrics.builder().push(push).build()); + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + }); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_PUSH, pushCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/redfish/RedfishCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/redfish/RedfishCollectImplTest.java new file mode 100644 index 0000000..d8fb214 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/redfish/RedfishCollectImplTest.java @@ -0,0 +1,195 @@ +/* + * 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.collector.collect.redfish; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.RedfishProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link RedfishCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +public class RedfishCollectImplTest { + @Mock + private RedfishProtocol redfishProtocol; + + @Mock + private ConnectSession redfishConnectSession; + + @Mock + private RedfishClient redfishClient; + + @InjectMocks + private RedfishCollectImpl redfishCollect; + + @BeforeEach + void setUp() { + redfishProtocol = RedfishProtocol.builder() + .host("https://127.0.0.1") + .port("5000") + .username("Administrator") + .password("Password") + .timeout("5000") + .build(); + } + + @Test + void collect() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + List<String> jsonPath = new ArrayList<>(); + jsonPath.add("$.Id"); + Metrics metrics = new Metrics(); + metrics.setRedfish(redfishProtocol); + metrics.getRedfish().setJsonPath(jsonPath); + metrics.setName("Chassis"); + RedfishClient.create(redfishProtocol); + redfishCollect.preCheck(metrics); + redfishCollect.collect(builder, metrics); + } + + @Test + void mockCollect() throws Exception { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + List<String> jsonPath = new ArrayList<>(); + jsonPath.add("$.['@odata.id']"); + redfishProtocol.setSchema("/redfish/v1/Chassis/{ChassisId}/PowerSubsystem/PowerSupplies"); + Metrics metrics = new Metrics(); + metrics.setRedfish(redfishProtocol); + metrics.getRedfish().setJsonPath(jsonPath); + metrics.setName("PowerSupply"); + String chassis = """ + { + "@odata.type": "#ChassisCollection.ChassisCollection", + "Name": "Chassis Collection", + "Members@odata.count": 2, + "Members": [ + { + "@odata.id": "/redfish/v1/Chassis/1U" + }, + { + "@odata.id": "/redfish/v1/Chassis/2U" + } + ] + }"""; + String powerSupplies1U = """ + { + "@odata.type": "#PowerSupplyCollection.PowerSupplyCollection", + "Name": "Power Supply Collection", + "Members@odata.count": 2, + "Members": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2" + } + ] + }"""; + String powerSupplies2U = """ + { + "@odata.type": "#PowerSupplyCollection.PowerSupplyCollection", + "Name": "Power Supply Collection", + "Members@odata.count": 2, + "Members": [ + { + "@odata.id": "/redfish/v1/Chassis/2U/PowerSubsystem/PowerSupplies/Bay1" + }, + { + "@odata.id": "/redfish/v1/Chassis/2U/PowerSubsystem/PowerSupplies/Bay2" + } + ] + }"""; + String bay1U1 = """ + { + "@odata.type": "#PowerSupply.v1_5_3.PowerSupply", + "Id": "Bay1", + "Name": "Power Supply Bay 1", + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1" + }"""; + String bay2U1 = """ + { + "@odata.type": "#PowerSupply.v1_5_3.PowerSupply", + "Id": "Bay2", + "Name": "Power Supply Bay 2", + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2" + }"""; + String bay1U2 = """ + { + "@odata.type": "#PowerSupply.v1_5_3.PowerSupply", + "Id": "Bay1", + "Name": "Power Supply Bay 1", + "@odata.id": "/redfish/v1/Chassis/2U/PowerSubsystem/PowerSupplies/Bay1" + }"""; + String bay2U2 = """ + { + "@odata.type": "#PowerSupply.v1_5_3.PowerSupply", + "Id": "Bay2", + "Name": "Power Supply Bay 2", + "@odata.id": "/redfish/v1/Chassis/2U/PowerSubsystem/PowerSupplies/Bay2" + }"""; + Mockito.when(redfishConnectSession.getRedfishResource("/redfish/v1/Chassis/")).thenReturn(chassis); + Mockito.when(redfishConnectSession.getRedfishResource("/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies")).thenReturn(powerSupplies1U); + Mockito.when(redfishConnectSession.getRedfishResource("/redfish/v1/Chassis/2U/PowerSubsystem/PowerSupplies")).thenReturn(powerSupplies2U); + Mockito.when(redfishConnectSession.getRedfishResource("/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1")).thenReturn(bay1U1); + Mockito.when(redfishConnectSession.getRedfishResource("/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2")).thenReturn(bay2U1); + Mockito.when(redfishConnectSession.getRedfishResource("/redfish/v1/Chassis/2U/PowerSubsystem/PowerSupplies/Bay1")).thenReturn(bay1U2); + Mockito.when(redfishConnectSession.getRedfishResource("/redfish/v1/Chassis/2U/PowerSubsystem/PowerSupplies/Bay2")).thenReturn(bay2U2); + MockedStatic<RedfishClient> clientMockedStatic = Mockito.mockStatic(RedfishClient.class); + clientMockedStatic.when(() -> RedfishClient.create(redfishProtocol)).thenReturn(redfishClient); + Mockito.when(redfishClient.connect()).thenReturn(redfishConnectSession); + redfishCollect.preCheck(metrics); + redfishCollect.collect(builder, metrics); + assertEquals("/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1", builder.getValues(0).getColumns(0)); + assertEquals("/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2", builder.getValues(1).getColumns(0)); + assertEquals("/redfish/v1/Chassis/2U/PowerSubsystem/PowerSupplies/Bay1", builder.getValues(2).getColumns(0)); + assertEquals("/redfish/v1/Chassis/2U/PowerSubsystem/PowerSupplies/Bay2", builder.getValues(3).getColumns(0)); + } + + @Test + void preCheck() throws Exception { + // metrics is null + assertThrows(IllegalArgumentException.class, () -> redfishCollect.preCheck(null)); + + // protocol is null + assertThrows(IllegalArgumentException.class, () -> { + Metrics metrics = Metrics.builder().build(); + redfishCollect.preCheck(metrics); + }); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_REDFISH, redfishCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/redis/RedisClusterCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/redis/RedisClusterCollectImplTest.java new file mode 100644 index 0000000..5d74e92 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/redis/RedisClusterCollectImplTest.java @@ -0,0 +1,170 @@ +/* + * 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.collector.collect.redis; + +import static org.apache.hertzbeat.common.constants.CommonConstants.TYPE_STRING; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; +import io.lettuce.core.cluster.models.partitions.Partitions; +import io.lettuce.core.cluster.models.partitions.RedisClusterNode; +import io.lettuce.core.resource.ClientResources; +import java.util.ArrayList; +import java.util.List; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.RedisProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link RedisCommonCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +public class RedisClusterCollectImplTest { + + @InjectMocks + private RedisCommonCollectImpl redisClusterCollect; + + + @Mock + private StatefulRedisClusterConnection<String, String> clusterConnection; + + @Mock + private StatefulRedisConnection<String, String> singleConnection; + + @Mock + private RedisCommands<String, String> cmd; + + @Mock + private RedisClusterClient clusterClient; + + @Mock + private RedisClient singleClient; + + @BeforeEach + void setUp() { + } + + @AfterEach + void setDown() { + clusterConnection.close(); + singleConnection.close(); + clusterClient.shutdown(); + singleClient.shutdown(); + } + + @Test + void testCollect() { + RedisProtocol redisProtocol = RedisProtocol.builder() + .host("127.0.0.1") + .port("6379") + .pattern("3") + .build(); + String infoTemp = """ + # Cluster + cluster_enabled:%s + """; + String clusterEnabled = "1"; + String info = String.format(infoTemp, clusterEnabled); + + String clusterKnownNodes = "2"; + String clusterInfoTemp = """ + cluster_slots_fail:0 + cluster_known_nodes:%s + """; + String clusterInfo = String.format(clusterInfoTemp, clusterKnownNodes); + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + List<String> aliasField = new ArrayList<>(); + aliasField.add("cluster_known_nodes"); + aliasField.add("cluster_enabled"); + aliasField.add("identity"); + List<Metrics.Field> fields = new ArrayList<>(); + fields.add(Metrics.Field.builder() + .field("cluster_enabled") + .type(TYPE_STRING) + .build()); + fields.add(Metrics.Field.builder() + .field("cluster_known_nodes") + .type(TYPE_STRING) + .build()); + + Metrics metrics = new Metrics(); + metrics.setName("cluster"); + metrics.setRedis(redisProtocol); + metrics.setAliasFields(aliasField); + metrics.setFields(fields); + + MockedStatic<RedisClusterClient> redisClusterClientMockedStatic = Mockito.mockStatic(RedisClusterClient.class); + redisClusterClientMockedStatic.when(() -> RedisClusterClient.create(Mockito.any(ClientResources.class), + Mockito.any(RedisURI.class))).thenReturn(clusterClient); + Mockito.when(clusterClient.connect()).thenReturn(clusterConnection); + MockedStatic<RedisClient> redisClientMockedStatic = Mockito.mockStatic(RedisClient.class); + redisClientMockedStatic.when(() -> RedisClient.create(Mockito.any(ClientResources.class), + Mockito.any(RedisURI.class))).thenReturn(singleClient); + Mockito.when(singleClient.connect()).thenReturn(singleConnection); + + Partitions partitions = new Partitions(); + RedisClusterNode node = new RedisClusterNode(); + String uri1 = "127.0.0.1:6379"; + node.setUri(RedisURI.create("redis://" + uri1)); + partitions.add(node); + RedisClusterNode node2 = new RedisClusterNode(); + String uri2 = "127.0.0.2:6379"; + node2.setUri(RedisURI.create("redis://" + uri2)); + partitions.add(node2); + + Mockito.when(clusterConnection.getPartitions()).thenReturn(partitions); + + Mockito.when(singleConnection.sync()).thenReturn(cmd); + Mockito.when(cmd.info(metrics.getName())).thenReturn(info); + Mockito.when(cmd.clusterInfo()).thenReturn(clusterInfo); + + redisClusterCollect.preCheck(metrics); + redisClusterCollect.collect(builder, metrics); + + assertEquals(builder.getCode(), CollectRep.Code.SUCCESS); + assertEquals(builder.getValuesCount(), 2); + for (int i = 0; i < builder.getValuesList().size(); i++) { + CollectRep.ValueRow row = builder.getValues(i); + assertEquals(row.getColumnsCount(), 3); + assertEquals(row.getColumns(0), clusterKnownNodes); + assertEquals(row.getColumns(1), clusterEnabled); + if (i == 0) { + assertEquals(row.getColumns(2), uri1); + } else { + assertEquals(row.getColumns(2), uri2); + } + } + redisClusterClientMockedStatic.close(); + redisClientMockedStatic.close(); + + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/redis/RedisSingleCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/redis/RedisSingleCollectImplTest.java new file mode 100644 index 0000000..118657a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/redis/RedisSingleCollectImplTest.java @@ -0,0 +1,162 @@ +/* + * 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.collector.collect.redis; + +import static org.apache.hertzbeat.common.constants.CommonConstants.TYPE_STRING; +import static org.junit.jupiter.api.Assertions.assertEquals; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.resource.ClientResources; +import java.util.ArrayList; +import java.util.List; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.RedisProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link RedisCommonCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +class RedisSingleCollectImplTest { + + + @Mock + private RedisProtocol redisProtocol; + + @Mock + private StatefulRedisConnection<String, String> connection; + + @Mock + private RedisCommands<String, String> cmd; + + @Mock + private RedisClient client; + + + @InjectMocks + private RedisCommonCollectImpl redisSingleCollect; + + @BeforeEach + void setUp() { + redisProtocol = RedisProtocol.builder() + .host("192.168.77.100") + .port("26379") + .pattern("1") + .build(); + } + + @AfterEach + void setDown() { + connection.close(); + client.shutdown(); + } + + @Test + void getInstance() { + } + + @Test + void collect() { + String info = """ + # CPU + used_cpu_sys:0.544635 + used_cpu_user:0.330690 + """; + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + List<String> aliasField = new ArrayList<>(); + aliasField.add("used_cpu_sys"); + Metrics metrics = new Metrics(); + metrics.setRedis(redisProtocol); + metrics.setName("cpu"); + metrics.setAliasFields(aliasField); + metrics.setFields(List.of()); + + MockedStatic<RedisClient> clientMockedStatic = Mockito.mockStatic(RedisClient.class); + clientMockedStatic.when(() -> RedisClient.create(Mockito.any(ClientResources.class), Mockito.any(RedisURI.class))) + .thenReturn(client); + Mockito.when(client.connect()).thenReturn(connection); + Mockito.when(connection.sync()).thenReturn(cmd); + Mockito.when(cmd.info(metrics.getName())).thenReturn(info); + redisSingleCollect.collect(builder, metrics); + assertEquals(builder.getValues(0).getColumns(0), "0.544635"); + clientMockedStatic.close(); + } + + @Test + void testCollect() { + String redisInfoTemplate = """ + # Server + redis_mode:%s + redis_version:%s + """; + String redisMode = "standalone"; + String version = "7.2.4"; + String redisInfo = String.format(redisInfoTemplate, redisMode, version); + + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + List<String> aliasField = new ArrayList<>(); + aliasField.add("redis_mode"); + aliasField.add("redis_version"); + + List<Metrics.Field> fields = new ArrayList<>(); + fields.add(Metrics.Field.builder() + .field("redis_mode") + .type(TYPE_STRING) + .build()); + fields.add(Metrics.Field.builder() + .field("redis_version") + .type(TYPE_STRING) + .build()); + + Metrics metrics = new Metrics(); + metrics.setName("server"); + metrics.setRedis(redisProtocol); + metrics.setAliasFields(aliasField); + metrics.setFields(fields); + + MockedStatic<RedisClient> clientMockedStatic = Mockito.mockStatic(RedisClient.class); + clientMockedStatic.when(() -> RedisClient.create(Mockito.any(ClientResources.class), Mockito.any(RedisURI.class))) + .thenReturn(client); + + Mockito.when(client.connect()).thenReturn(connection); + Mockito.when(connection.sync()).thenReturn(cmd); + Mockito.when(cmd.info(metrics.getName())).thenReturn(redisInfo); + + redisSingleCollect.preCheck(metrics); + redisSingleCollect.collect(builder, metrics); + assertEquals(builder.getCode(), CollectRep.Code.SUCCESS); + for (CollectRep.ValueRow row : builder.getValuesList()) { + assertEquals(row.getColumnsCount(), 2); + assertEquals(row.getColumns(0), redisMode); + assertEquals(row.getColumns(1), version); + } + clientMockedStatic.close(); + client.shutdown(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/registry/RegistryImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/registry/RegistryImplTest.java new file mode 100644 index 0000000..7c0d517 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/registry/RegistryImplTest.java @@ -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.collector.collect.registry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.util.ArrayList; +import java.util.List; +import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClient; +import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClientManagement; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServerInfo; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link RegistryImpl} + */ +@ExtendWith(MockitoExtension.class) +class RegistryImplTest { + @InjectMocks + @Spy + private RegistryImpl registry; + + @Mock + private DiscoveryClient client; + + @Mock + private DiscoveryClientManagement discoveryClientManagement; + + @Test + void testServerCollect() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + + String port = "123"; + String host = "127.0.0.1"; + RegistryProtocol registryProtocol = RegistryProtocol.builder() + .port(port) + .host(host) + .discoveryClientTypeName("consul") + .build(); + List<String> aliasField = new ArrayList<>(); + aliasField.add("address"); + aliasField.add("port"); + aliasField.add("responseTime"); + Metrics metrics = new Metrics(); + metrics.setName("server"); + metrics.setRegistry(registryProtocol); + metrics.setAliasFields(aliasField); + + Mockito.when(discoveryClientManagement.getClient(registryProtocol)).thenReturn(client); + ServerInfo serverInfo = ServerInfo.builder() + .address(host) + .port(port) + .build(); + Mockito.when(client.getServerInfo()).thenReturn(serverInfo); + registry.setDiscoveryClientManagement(discoveryClientManagement); + registry.preCheck(metrics); + registry.collect(builder, metrics); + for (CollectRep.ValueRow valueRow : builder.getValuesList()) { + assertEquals(host, valueRow.getColumns(0)); + assertEquals(port, valueRow.getColumns(1)); + assertNotNull(valueRow.getColumns(2)); + } + } + + @Test + void testServiceCollect() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + + String port = "123"; + String host = "127.0.0.1"; + RegistryProtocol registryProtocol = RegistryProtocol.builder() + .port(port) + .host(host) + .discoveryClientTypeName("consul") + .build(); + List<String> aliasField = new ArrayList<>(); + aliasField.add("serviceId"); + aliasField.add("serviceName"); + aliasField.add("address"); + aliasField.add("port"); + Metrics metrics = new Metrics(); + metrics.setName("service"); + metrics.setRegistry(registryProtocol); + metrics.setAliasFields(aliasField); + + Mockito.when(discoveryClientManagement.getClient(registryProtocol)).thenReturn(client); + + String serviceId = "test"; + String serviceName = "service"; + List<ServiceInstance> serviceInstances = new ArrayList<>(); + serviceInstances.add(ServiceInstance.builder() + .serviceId(serviceId) + .serviceName(serviceName) + .address(host) + .port(Integer.parseInt(port)) + .build()); + + Mockito.when(client.getServices()).thenReturn(serviceInstances); + registry.setDiscoveryClientManagement(discoveryClientManagement); + registry.preCheck(metrics); + registry.collect(builder, metrics); + assertEquals(builder.getValuesCount(), 1); + for (CollectRep.ValueRow valueRow : builder.getValuesList()) { + assertEquals(serviceId, valueRow.getColumns(0)); + assertEquals(serviceName, valueRow.getColumns(1)); + assertEquals(host, valueRow.getColumns(2)); + assertEquals(port, valueRow.getColumns(3)); + } + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/registry/discovery/impl/NacosDiscoveryClientTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/registry/discovery/impl/NacosDiscoveryClientTest.java new file mode 100644 index 0000000..71a005d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/registry/discovery/impl/NacosDiscoveryClientTest.java @@ -0,0 +1,396 @@ +/* + * 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.collector.collect.registry.discovery.impl; + +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.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.naming.NamingFactory; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.pojo.Instance; +import com.alibaba.nacos.api.naming.pojo.ListView; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import org.apache.hertzbeat.collector.collect.registry.constant.DiscoveryClientHealthStatus; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ConnectConfig; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServerInfo; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance; +import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol; +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.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Test case for {@link NacosDiscoveryClient} + */ +@ExtendWith(MockitoExtension.class) +class NacosDiscoveryClientTest { + + private NacosDiscoveryClient nacosDiscoveryClient; + + @Mock + private NamingService namingService; + + private static final String HOST = "127.0.0.1"; + private static final int PORT = 8848; + + @BeforeEach + void setUp() { + nacosDiscoveryClient = new NacosDiscoveryClient(); + } + + @Test + void testBuildConnectConfig() { + RegistryProtocol protocol = RegistryProtocol.builder() + .host(HOST) + .port(String.valueOf(PORT)) + .username("test-user") + .password("test-pass") + .namespace("test-namespace") + .serviceName("test-service") + .groupName("test-group") + .build(); + + ConnectConfig config = nacosDiscoveryClient.buildConnectConfig(protocol); + + assertNotNull(config); + assertEquals(HOST, config.getHost()); + assertEquals(PORT, config.getPort()); + assertEquals("test-user", config.getUsername()); + assertEquals("test-pass", config.getPassword()); + assertEquals("test-namespace", config.getNamespace()); + assertEquals("test-service", config.getServiceName()); + assertEquals("test-group", config.getGroupName()); + } + + @Test + void testInitClientSuccess() throws NacosException { + ConnectConfig config = ConnectConfig.builder().host(HOST).port(PORT).build(); + + try (MockedStatic<NamingFactory> mockedFactory = Mockito.mockStatic(NamingFactory.class)) { + mockedFactory.when(() -> NamingFactory.createNamingService(any(Properties.class))) + .thenReturn(namingService); + ListView<String> emptyView = new ListView<>(); + emptyView.setData(Collections.emptyList()); + when(namingService.getServicesOfServer(0, 1)).thenReturn(emptyView); + + nacosDiscoveryClient.initClient(config); + + mockedFactory.verify(() -> NamingFactory.createNamingService(any(Properties.class))); + verify(namingService).getServicesOfServer(0, 1); + } + } + + @Test + void testInitClientFailedOnCreate() { + ConnectConfig config = ConnectConfig.builder().host(HOST).port(PORT).build(); + + try (MockedStatic<NamingFactory> mockedFactory = Mockito.mockStatic(NamingFactory.class)) { + mockedFactory.when(() -> NamingFactory.createNamingService(any(Properties.class))) + .thenThrow(new NacosException(500, "connection refused")); + + assertThrows(RuntimeException.class, () -> nacosDiscoveryClient.initClient(config)); + } + } + + @Test + void testInitClientFailedOnProbe() throws NacosException { + ConnectConfig config = ConnectConfig.builder().host(HOST).port(PORT).build(); + + try (MockedStatic<NamingFactory> mockedFactory = Mockito.mockStatic(NamingFactory.class)) { + mockedFactory.when(() -> NamingFactory.createNamingService(any(Properties.class))) + .thenReturn(namingService); + when(namingService.getServicesOfServer(0, 1)) + .thenThrow(new NacosException(500, "host unreachable")); + + RuntimeException ex = assertThrows(RuntimeException.class, () -> nacosDiscoveryClient.initClient(config)); + assertTrue(ex.getMessage().contains("Failed to connect")); + } + } + + @Test + void testGetServerInfoWithNullNamingService() { + assertThrows(NullPointerException.class, () -> nacosDiscoveryClient.getServerInfo()); + } + + @Test + void testGetServerInfoWhenHealthy() { + injectNamingServiceAndConfig(); + when(namingService.getServerStatus()).thenReturn(DiscoveryClientHealthStatus.UP); + + ServerInfo serverInfo = nacosDiscoveryClient.getServerInfo(); + + assertNotNull(serverInfo); + assertEquals(HOST, serverInfo.getAddress()); + assertEquals(String.valueOf(PORT), serverInfo.getPort()); + } + + @Test + void testGetServerInfoWhenNotHealthy() { + injectNamingServiceAndConfig(); + when(namingService.getServerStatus()).thenReturn(DiscoveryClientHealthStatus.DOWN); + + assertThrows(RuntimeException.class, () -> nacosDiscoveryClient.getServerInfo()); + } + + @Test + void testGetServicesWithNullNamingService() { + List<ServiceInstance> result = nacosDiscoveryClient.getServices(); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void testGetServicesWhenNotHealthy() { + injectNamingServiceAndConfig(); + when(namingService.getServerStatus()).thenReturn(DiscoveryClientHealthStatus.DOWN); + + List<ServiceInstance> result = nacosDiscoveryClient.getServices(); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void testGetServicesSuccess() throws NacosException { + injectNamingServiceAndConfig(); + when(namingService.getServerStatus()).thenReturn(DiscoveryClientHealthStatus.UP); + + ListView<String> serviceNames = new ListView<>(); + serviceNames.setData(Collections.singletonList("test-service")); + when(namingService.getServicesOfServer(0, 9999)).thenReturn(serviceNames); + + Instance instance = new Instance(); + instance.setInstanceId("inst-1"); + instance.setServiceName("test-service"); + instance.setIp("192.168.1.1"); + instance.setPort(9090); + instance.setWeight(1.0); + Map<String, String> metadata = new HashMap<>(); + metadata.put("version", "1.0"); + instance.setMetadata(metadata); + instance.setHealthy(true); + + when(namingService.getAllInstances("test-service")).thenReturn(Collections.singletonList(instance)); + + List<ServiceInstance> result = nacosDiscoveryClient.getServices(); + + assertNotNull(result); + assertEquals(1, result.size()); + ServiceInstance serviceInstance = result.get(0); + assertEquals("inst-1", serviceInstance.getServiceId()); + assertEquals("test-service", serviceInstance.getServiceName()); + assertEquals("192.168.1.1", serviceInstance.getAddress()); + assertEquals(9090, serviceInstance.getPort()); + assertEquals(DiscoveryClientHealthStatus.UP, serviceInstance.getHealthStatus()); + } + + @Test + void testGetServicesWithUnhealthyInstance() throws NacosException { + injectNamingServiceAndConfig(); + when(namingService.getServerStatus()).thenReturn(DiscoveryClientHealthStatus.UP); + + ListView<String> serviceNames = new ListView<>(); + serviceNames.setData(Collections.singletonList("test-service")); + when(namingService.getServicesOfServer(0, 9999)).thenReturn(serviceNames); + + Instance instance = new Instance(); + instance.setInstanceId("inst-down"); + instance.setServiceName("test-service"); + instance.setIp("10.0.0.1"); + instance.setPort(8080); + instance.setWeight(1.0); + instance.setHealthy(false); + + when(namingService.getAllInstances("test-service")).thenReturn(Collections.singletonList(instance)); + + List<ServiceInstance> result = nacosDiscoveryClient.getServices(); + + assertEquals(1, result.size()); + assertEquals(DiscoveryClientHealthStatus.DOWN, result.get(0).getHealthStatus()); + } + + @Test + void testGetServicesThrowsNacosException() throws NacosException { + injectNamingServiceAndConfig(); + when(namingService.getServerStatus()).thenReturn(DiscoveryClientHealthStatus.UP); + when(namingService.getServicesOfServer(anyInt(), anyInt())) + .thenThrow(new NacosException(500, "server error")); + + assertThrows(RuntimeException.class, () -> nacosDiscoveryClient.getServices()); + } + + @Test + void testGetServicesWithGroupNameOnly() throws NacosException { + ConnectConfig config = ConnectConfig.builder().host(HOST).port(PORT).groupName("test-group").build(); + injectNamingServiceAndConfig(config); + when(namingService.getServerStatus()).thenReturn(DiscoveryClientHealthStatus.UP); + + ListView<String> serviceNames = new ListView<>(); + serviceNames.setData(Collections.singletonList("test-service")); + when(namingService.getServicesOfServer(0, 9999, "test-group")).thenReturn(serviceNames); + + Instance instance = new Instance(); + instance.setInstanceId("inst-2"); + instance.setServiceName("test-service"); + instance.setIp("192.168.1.2"); + instance.setPort(9090); + instance.setWeight(1.0); + instance.setHealthy(true); + + when(namingService.getAllInstances("test-service", "test-group")).thenReturn(Collections.singletonList(instance)); + + List<ServiceInstance> result = nacosDiscoveryClient.getServices(); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("inst-2", result.get(0).getServiceId()); + + verify(namingService).getServicesOfServer(0, 9999, "test-group"); + verify(namingService).getAllInstances("test-service", "test-group"); + } + + @Test + void testGetServicesWithServiceNameOnly() throws NacosException { + ConnectConfig config = ConnectConfig.builder().host(HOST).port(PORT).serviceName("test-service-only").build(); + injectNamingServiceAndConfig(config); + when(namingService.getServerStatus()).thenReturn(DiscoveryClientHealthStatus.UP); + + Instance instance = new Instance(); + instance.setInstanceId("inst-3"); + instance.setServiceName("test-service-only"); + instance.setIp("192.168.1.3"); + instance.setPort(9090); + instance.setWeight(1.0); + instance.setHealthy(true); + + when(namingService.getAllInstances("test-service-only")).thenReturn(Collections.singletonList(instance)); + + List<ServiceInstance> result = nacosDiscoveryClient.getServices(); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("inst-3", result.get(0).getServiceId()); + + // Assert getServicesOfServer is NOT called + verify(namingService, never()).getServicesOfServer(anyInt(), anyInt()); + verify(namingService, never()).getServicesOfServer(anyInt(), anyInt(), anyString()); + + verify(namingService).getAllInstances("test-service-only"); + } + + @Test + void testGetServicesWithGroupAndServiceName() throws NacosException { + ConnectConfig config = ConnectConfig.builder().host(HOST).port(PORT).groupName("test-group").serviceName("target-service").build(); + injectNamingServiceAndConfig(config); + when(namingService.getServerStatus()).thenReturn(DiscoveryClientHealthStatus.UP); + + Instance instance = new Instance(); + instance.setInstanceId("inst-4"); + instance.setServiceName("target-service"); + instance.setIp("192.168.1.4"); + instance.setPort(9090); + instance.setWeight(1.0); + instance.setHealthy(true); + + when(namingService.getAllInstances("target-service", "test-group")).thenReturn(Collections.singletonList(instance)); + + List<ServiceInstance> result = nacosDiscoveryClient.getServices(); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("inst-4", result.get(0).getServiceId()); + + // Assert getServicesOfServer is NOT called + verify(namingService, never()).getServicesOfServer(anyInt(), anyInt()); + verify(namingService, never()).getServicesOfServer(anyInt(), anyInt(), anyString()); + + verify(namingService).getAllInstances("target-service", "test-group"); + } + + @Test + void testHealthCheckReturnsTrue() { + injectNamingServiceAndConfig(); + when(namingService.getServerStatus()).thenReturn(DiscoveryClientHealthStatus.UP); + + assertTrue(nacosDiscoveryClient.healthCheck()); + } + + @Test + void testHealthCheckReturnsFalse() { + injectNamingServiceAndConfig(); + when(namingService.getServerStatus()).thenReturn(DiscoveryClientHealthStatus.DOWN); + + assertFalse(nacosDiscoveryClient.healthCheck()); + } + + @Test + void testCloseWithNullNamingService() { + nacosDiscoveryClient.close(); + } + + @Test + void testCloseSuccess() throws NacosException { + injectNamingServiceAndConfig(); + + nacosDiscoveryClient.close(); + + verify(namingService).shutDown(); + } + + @Test + void testCloseThrowsNacosException() throws NacosException { + injectNamingServiceAndConfig(); + doThrow(new NacosException(500, "shutdown error")).when(namingService).shutDown(); + + nacosDiscoveryClient.close(); + + verify(namingService).shutDown(); + } + + private void injectNamingServiceAndConfig() { + injectNamingServiceAndConfig(ConnectConfig.builder().host(HOST).port(PORT).build()); + } + + private void injectNamingServiceAndConfig(ConnectConfig config) { + ReflectionTestUtils.setField(nacosDiscoveryClient, "namingService", namingService); + ReflectionTestUtils.setField(nacosDiscoveryClient, "localConnectConfig", config); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/script/ScriptCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/script/ScriptCollectImplTest.java new file mode 100644 index 0000000..f51fbd5 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/script/ScriptCollectImplTest.java @@ -0,0 +1,147 @@ +/* + * 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.collector.collect.script; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.ScriptProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link ScriptCollectImpl} + */ +public class ScriptCollectImplTest { + private ScriptCollectImpl scriptCollect; + private CollectRep.MetricsData.Builder builder; + + @BeforeEach + void setUp() throws Exception { + scriptCollect = new ScriptCollectImpl(); + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void preCheck() { + // metrics is null + assertThrows(IllegalArgumentException.class, () -> { + scriptCollect.preCheck(null); + }); + + // protocol is null + assertThrows(IllegalArgumentException.class, () -> { + Metrics metrics = new Metrics(); + scriptCollect.preCheck(metrics); + }); + + // charset is null + assertThrows(IllegalArgumentException.class, () -> { + ScriptProtocol scriptProtocol = ScriptProtocol.builder().build(); + Metrics metrics = new Metrics(); + metrics.setScript(scriptProtocol); + scriptCollect.preCheck(metrics); + }); + + // parse type is null + assertThrows(IllegalArgumentException.class, () -> { + ScriptProtocol scriptProtocol = ScriptProtocol.builder().charset("utf-8").build(); + Metrics metrics = new Metrics(); + metrics.setScript(scriptProtocol); + scriptCollect.preCheck(metrics); + }); + + // script tool is null + assertThrows(IllegalArgumentException.class, () -> { + ScriptProtocol scriptProtocol = ScriptProtocol.builder().charset("utf-8").parseType("json").build(); + Metrics metrics = new Metrics(); + metrics.setScript(scriptProtocol); + scriptCollect.preCheck(metrics); + }); + + // script command is null + assertThrows(IllegalArgumentException.class, () -> { + ScriptProtocol scriptProtocol = ScriptProtocol.builder().charset("utf-8").parseType("json").scriptTool("sh").build(); + Metrics metrics = new Metrics(); + metrics.setScript(scriptProtocol); + scriptCollect.preCheck(metrics); + }); + + // script path is null + assertDoesNotThrow(() -> { + ScriptProtocol scriptProtocol = ScriptProtocol.builder().charset("utf-8").parseType("json").scriptTool("sh").scriptCommand("ls").build(); + Metrics metrics = new Metrics(); + metrics.setScript(scriptProtocol); + scriptCollect.preCheck(metrics); + }); + + // everything is ok + assertDoesNotThrow(() -> { + ScriptProtocol scriptProtocol = ScriptProtocol.builder().charset("utf-8").parseType("json").scriptTool("sh").scriptPath("/tmp").build(); + Metrics metrics = new Metrics(); + metrics.setScript(scriptProtocol); + scriptCollect.preCheck(metrics); + }); + } + + @Test + void collect() { + // not support script tool with command + assertDoesNotThrow(() -> { + ScriptProtocol scriptProtocol = ScriptProtocol.builder().charset("utf-8").parseType("json").scriptTool("sh").scriptCommand("cmd").build(); + Metrics metrics = new Metrics(); + metrics.setScript(scriptProtocol); + + builder = CollectRep.MetricsData.newBuilder(); + scriptCollect.collect(builder, metrics); + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + }); + + + // not support script tool with scriptpath + assertDoesNotThrow(() -> { + ScriptProtocol scriptProtocol = ScriptProtocol.builder().charset("utf-8").parseType("json").scriptTool("sh").scriptPath("cmd").build(); + Metrics metrics = new Metrics(); + metrics.setScript(scriptProtocol); + + builder = CollectRep.MetricsData.newBuilder(); + scriptCollect.collect(builder, metrics); + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + }); + + // without script command and path + assertDoesNotThrow(() -> { + ScriptProtocol scriptProtocol = ScriptProtocol.builder().charset("utf-8").parseType("json").build(); + Metrics metrics = new Metrics(); + metrics.setScript(scriptProtocol); + + builder = CollectRep.MetricsData.newBuilder(); + scriptCollect.collect(builder, metrics); + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + }); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_SCRIPT, scriptCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/sd/HttpSdCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/sd/HttpSdCollectImplTest.java new file mode 100644 index 0000000..e8b8155 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/sd/HttpSdCollectImplTest.java @@ -0,0 +1,200 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.sd; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import lombok.SneakyThrows; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.entity.sd.ServiceDiscoveryResponseEntity; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.http.HttpEntity; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicStatusLine; +import org.apache.http.protocol.HttpContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link HttpSdCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +class HttpSdCollectImplTest { + + @InjectMocks + private HttpSdCollectImpl httpSdCollectImpl; + + @Mock + private CloseableHttpClient httpClient; + + @Mock + private CloseableHttpResponse httpResponse; + + private Metrics metrics; + private HttpProtocol httpProtocol; + private CollectRep.MetricsData.Builder builder; + + @BeforeEach + void setUp() { + httpProtocol = HttpProtocol.builder() + .url("http://localhost:8080/services") + .build(); + + metrics = Metrics.builder() + .http_sd(httpProtocol) + .build(); + + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void testCollectSuccess() throws IOException { + List<ServiceDiscoveryResponseEntity> responseEntities = new ArrayList<>(); + + ServiceDiscoveryResponseEntity entity1 = new ServiceDiscoveryResponseEntity(); + entity1.setTarget(Arrays.asList("192.168.1.10:8080", "192.168.1.11:8081")); + + ServiceDiscoveryResponseEntity entity2 = new ServiceDiscoveryResponseEntity(); + entity2.setTarget(Arrays.asList("192.168.1.12:8082")); + + responseEntities.add(entity1); + responseEntities.add(entity2); + + String jsonResponse = JsonUtil.toJson(responseEntities); + + try (MockedStatic<CommonHttpClient> mockedHttpClient = Mockito.mockStatic(CommonHttpClient.class)) { + mockedHttpClient.when(CommonHttpClient::getHttpClient).thenReturn(httpClient); + + when(httpClient.execute(any(HttpUriRequest.class), nullable(HttpContext.class))).thenReturn(httpResponse); + + StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + + HttpEntity entity = new StringEntity(jsonResponse, ContentType.APPLICATION_JSON); + when(httpResponse.getEntity()).thenReturn(entity); + + httpSdCollectImpl.collect(builder, metrics); + + assertEquals(3, builder.getValuesCount()); + + CollectRep.ValueRow firstRow = builder.getValues(0); + assertEquals("192.168.1.10", firstRow.getColumns(0)); + assertEquals("8080", firstRow.getColumns(1)); + + CollectRep.ValueRow secondRow = builder.getValues(1); + assertEquals("192.168.1.11", secondRow.getColumns(0)); + assertEquals("8081", secondRow.getColumns(1)); + + CollectRep.ValueRow thirdRow = builder.getValues(2); + assertEquals("192.168.1.12", thirdRow.getColumns(0)); + assertEquals("8082", thirdRow.getColumns(1)); + } + } + + @Test + void testCollectWithNon200Status() throws IOException { + try (MockedStatic<CommonHttpClient> mockedHttpClient = Mockito.mockStatic(CommonHttpClient.class)) { + mockedHttpClient.when(CommonHttpClient::getHttpClient).thenReturn(httpClient); + + when(httpClient.execute(any(HttpUriRequest.class), nullable(HttpContext.class))).thenReturn(httpResponse); + + StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 404, "Not Found"); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + + httpSdCollectImpl.collect(builder, metrics); + + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + assertEquals("StatusCode 404", builder.getMsg()); + } + } + + @SneakyThrows + @Test + void testCollectWithException() { + String exceptionMessage = "Connection refused"; + IOException testException = new IOException(exceptionMessage); + + try (MockedStatic<CommonHttpClient> mockedHttpClient = Mockito.mockStatic(CommonHttpClient.class); + MockedStatic<CommonUtil> mockedCommonUtil = Mockito.mockStatic(CommonUtil.class)) { + + mockedHttpClient.when(CommonHttpClient::getHttpClient).thenReturn(httpClient); + + when(httpClient.execute(any(HttpUriRequest.class), nullable(HttpContext.class))).thenThrow(testException); + mockedCommonUtil.when(() -> CommonUtil.getMessageFromThrowable(any(Throwable.class))) + .thenReturn(exceptionMessage); + + httpSdCollectImpl.collect(builder, metrics); + + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + assertEquals(exceptionMessage, builder.getMsg()); + } + } + + @Test + void testCollectWithEmptyServiceList() throws IOException { + List<ServiceDiscoveryResponseEntity> emptyList = Collections.emptyList(); + String jsonResponse = JsonUtil.toJson(emptyList); + + try (MockedStatic<CommonHttpClient> mockedHttpClient = Mockito.mockStatic(CommonHttpClient.class)) { + mockedHttpClient.when(CommonHttpClient::getHttpClient).thenReturn(httpClient); + + when(httpClient.execute(any(HttpUriRequest.class), nullable(HttpContext.class))).thenReturn(httpResponse); + + StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + + HttpEntity entity = new StringEntity(jsonResponse, ContentType.APPLICATION_JSON); + when(httpResponse.getEntity()).thenReturn(entity); + + httpSdCollectImpl.collect(builder, metrics); + + assertEquals(0, builder.getValuesCount()); + } + } + + @Test + void testSupportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_HTTP_SD, httpSdCollectImpl.supportProtocol()); + } +} \ No newline at end of file diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/sd/NacosSdCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/sd/NacosSdCollectImplTest.java new file mode 100644 index 0000000..6fe312a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/sd/NacosSdCollectImplTest.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.sd; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import lombok.SneakyThrows; +import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClient; +import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClientManagement; +import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.NacosSdProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Test case for {@link NacosSdCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +class NacosSdCollectImplTest { + + @InjectMocks + private NacosSdCollectImpl nacosSdCollectImpl; + + @Mock + private DiscoveryClientManagement discoveryClientManagement; + + @Mock + private DiscoveryClient discoveryClient; + + private Metrics metrics; + private NacosSdProtocol nacosSdProtocol; + private CollectRep.MetricsData.Builder builder; + + @BeforeEach + void setUp() { + nacosSdProtocol = NacosSdProtocol.builder() + .host("localhost") + .port("8848") + .username("nacos") + .password("nacos") + .build(); + + metrics = Metrics.builder() + .nacos_sd(nacosSdProtocol) + .build(); + + builder = CollectRep.MetricsData.newBuilder(); + + ReflectionTestUtils.setField(nacosSdCollectImpl, "discoveryClientManagement", discoveryClientManagement); + } + + @Test + void testPreCheckWithNullMetrics() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> nacosSdCollectImpl.preCheck(null)); + + assertEquals("Nacos service discovery monitoring, the config is null", exception.getMessage()); + } + + @Test + void testPreCheckWithNullNacosSdConfig() { + Metrics metricsWithoutNacosSd = Metrics.builder().build(); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> nacosSdCollectImpl.preCheck(metricsWithoutNacosSd)); + + assertEquals("Nacos service discovery monitoring, the config is null", exception.getMessage()); + } + + @Test + void testPreCheckWithInvalidNacosSdConfig() { + NacosSdProtocol invalidNacosSdProtocol = NacosSdProtocol.builder() + .host("") + .port("8848") + .build(); + + Metrics metricsWithInvalidNacosSd = Metrics.builder() + .nacos_sd(invalidNacosSdProtocol) + .build(); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> nacosSdCollectImpl.preCheck(metricsWithInvalidNacosSd)); + + assertEquals("Nacos service discovery monitoring, the config is invalid", exception.getMessage()); + + NacosSdProtocol portInvalidNacosSdProtocol = NacosSdProtocol.builder() + .host("localhost") + .port("") + .build(); + + Metrics portMetricsWithInvalidNacosSd = Metrics.builder() + .nacos_sd(portInvalidNacosSdProtocol) + .build(); + + exception = assertThrows(IllegalArgumentException.class, + () -> nacosSdCollectImpl.preCheck(portMetricsWithInvalidNacosSd)); + + assertEquals("Nacos service discovery monitoring, the config is invalid", exception.getMessage()); + } + + @Test + void testPreCheckWithValidConfig() { + assertDoesNotThrow(() -> nacosSdCollectImpl.preCheck(metrics)); + } + + @Test + void testCollectSuccess() throws Exception { + List<ServiceInstance> services = new ArrayList<>(); + services.add(ServiceInstance.builder() + .serviceId("service-1") + .serviceName("user-service") + .address("192.168.1.10") + .port(8080) + .healthStatus("UP") + .build()); + services.add(ServiceInstance.builder() + .serviceId("service-2") + .serviceName("order-service") + .address("192.168.1.11") + .port(8081) + .healthStatus("UP") + .build()); + + when(discoveryClientManagement.getClient(any(RegistryProtocol.class))).thenReturn(discoveryClient); + + when(discoveryClient.getServices()).thenReturn(services); + + nacosSdCollectImpl.collect(builder, metrics); + + + verify(discoveryClient, times(1)).close(); + + assertEquals(2, builder.getValuesCount()); + + CollectRep.ValueRow firstRow = builder.getValues(0); + assertEquals("192.168.1.10", firstRow.getColumns(0)); + assertEquals("8080", firstRow.getColumns(1)); + assertEquals("user-service", firstRow.getColumns(2)); + assertEquals("UP", firstRow.getColumns(3)); + + CollectRep.ValueRow secondRow = builder.getValues(1); + assertEquals("192.168.1.11", secondRow.getColumns(0)); + assertEquals("8081", secondRow.getColumns(1)); + assertEquals("order-service", secondRow.getColumns(2)); + assertEquals("UP", secondRow.getColumns(3)); + } + + @Test + void testCollectClientNull() { + when(discoveryClientManagement.getClient(any(RegistryProtocol.class))).thenReturn(null); + + nacosSdCollectImpl.collect(builder, metrics); + + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + assertEquals("Failed to get Nacos discovery client", builder.getMsg()); + } + + @SneakyThrows + @Test + void testCollectWithException() { + String exceptionMessage = "Connection refused"; + Exception testException = new RuntimeException(exceptionMessage); + + when(discoveryClientManagement.getClient(any(RegistryProtocol.class))).thenReturn(discoveryClient); + when(discoveryClient.getServices()).thenThrow(testException); + + try (MockedStatic<CommonUtil> mockedCommonUtil = Mockito.mockStatic(CommonUtil.class)) { + mockedCommonUtil.when(() -> CommonUtil.getMessageFromThrowable(any(Throwable.class))) + .thenReturn(exceptionMessage); + + nacosSdCollectImpl.collect(builder, metrics); + + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + assertEquals(exceptionMessage, builder.getMsg()); + + verify(discoveryClient, times(1)).close(); + } + } + + @Test + void testCollectWithEmptyServiceList() throws Exception { + when(discoveryClientManagement.getClient(any(RegistryProtocol.class))).thenReturn(discoveryClient); + when(discoveryClient.getServices()).thenReturn(Collections.emptyList()); + + nacosSdCollectImpl.collect(builder, metrics); + + assertEquals(0, builder.getValuesCount()); + + verify(discoveryClient, times(1)).close(); + } + + @Test + void testSupportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_NACOS_SD, nacosSdCollectImpl.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/sd/ZookeeperSdCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/sd/ZookeeperSdCollectImplTest.java new file mode 100644 index 0000000..df18a6b --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/sd/ZookeeperSdCollectImplTest.java @@ -0,0 +1,113 @@ +/* + * 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.collector.collect.sd; + +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.ZookeeperSdProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.zookeeper.ZooKeeper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +@ExtendWith(MockitoExtension.class) +class ZookeeperSdCollectImplTest { + + private ZookeeperSdCollectImpl zookeeperSdCollect; + + private Metrics metrics; + private ZookeeperSdProtocol protocol; + private CollectRep.MetricsData.Builder builder; + + @BeforeEach + void setUp() { + zookeeperSdCollect = new ZookeeperSdCollectImpl(); + + protocol = ZookeeperSdProtocol.builder() + .url("localhost:2181") + .pathPrefix("/services") + .build(); + + metrics = Metrics.builder() + .zookeeper_sd(protocol) + .build(); + + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void testPreCheckWithNullConfig() { + Metrics invalid = Metrics.builder().build(); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> zookeeperSdCollect.preCheck(invalid)); + assertEquals("Zookeeper SD configuration cannot be null", e.getMessage()); + } + + @Test + void testPreCheckWithEmptyUrl() { + ZookeeperSdProtocol badProtocol = ZookeeperSdProtocol.builder().url("").pathPrefix("/path").build(); + Metrics m = Metrics.builder().zookeeper_sd(badProtocol).build(); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> zookeeperSdCollect.preCheck(m)); + assertEquals("Zookeeper URL cannot be null or empty", e.getMessage()); + } + + @Test + void testPreCheckWithEmptyPath() { + ZookeeperSdProtocol badProtocol = ZookeeperSdProtocol.builder().url("host").pathPrefix("").build(); + Metrics m = Metrics.builder().zookeeper_sd(badProtocol).build(); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> zookeeperSdCollect.preCheck(m)); + assertEquals("Zookeeper path prefix cannot be null or empty", e.getMessage()); + } + + @Test + void testPreCheckValid() { + assertDoesNotThrow(() -> zookeeperSdCollect.preCheck(metrics)); + } + + @Test + void testSupportProtocol() { + assertEquals("zookeeper_sd", zookeeperSdCollect.supportProtocol()); + } + + @Test + void testCollectSuccess() throws Exception { + try (MockedConstruction<ZooKeeper> mocked = Mockito.mockConstruction(ZooKeeper.class, + (mock, context) -> Mockito.when(mock.getChildren("/services", false)).thenReturn(List.of("host1:8080", "host2:9090")))) { + + zookeeperSdCollect.collect(builder, metrics); + + assertEquals(2, builder.getValuesCount()); + assertEquals("host1", builder.getValues(0).getColumns(0)); + assertEquals("8080", builder.getValues(0).getColumns(1)); + assertEquals("host2", builder.getValues(1).getColumns(0)); + assertEquals("9090", builder.getValues(1).getColumns(1)); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/smtp/SmtpCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/smtp/SmtpCollectImplTest.java new file mode 100644 index 0000000..47a0f72 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/smtp/SmtpCollectImplTest.java @@ -0,0 +1,80 @@ +/* + * 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.collector.collect.smtp; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.SmtpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link SmtpCollectImpl} + */ +public class SmtpCollectImplTest { + private SmtpCollectImpl smtpCollect; + private CollectRep.MetricsData.Builder builder; + + @BeforeEach + void setup() { + smtpCollect = new SmtpCollectImpl(); + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void preCheck() { + // metrics is null + assertThrows(IllegalArgumentException.class, () -> { + smtpCollect.preCheck(null); + }); + + // stmp protocol is null + assertThrows(IllegalArgumentException.class, () -> { + smtpCollect.preCheck(new Metrics()); + }); + + // everthing is ok + assertDoesNotThrow(() -> { + Metrics metrics = Metrics.builder() + .smtp(new SmtpProtocol()) + .build(); + smtpCollect.preCheck(metrics); + }); + } + + @Test + void collect() { + assertDoesNotThrow(() -> { + Metrics metrics = Metrics.builder() + .smtp(new SmtpProtocol()) + .build(); + smtpCollect.collect(builder, metrics); + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + }); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_SMTP, smtpCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/snmp/SnmpCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/snmp/SnmpCollectImplTest.java new file mode 100644 index 0000000..f9e2941 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/snmp/SnmpCollectImplTest.java @@ -0,0 +1,108 @@ +/* + * 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.collector.collect.snmp; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.SnmpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link SnmpCollectImpl} + */ +class SnmpCollectImplTest { + private SnmpCollectImpl snmpCollect; + private Metrics metrics; + private CollectRep.MetricsData.Builder builder; + + @BeforeEach + void setUp() { + snmpCollect = new SnmpCollectImpl(); + metrics = new Metrics(); + SnmpProtocol snmap = new SnmpProtocol(); + snmap.setHost("127.0.0.1"); + snmap.setPort("161"); + snmap.setVersion("2c"); + metrics.setSnmp(snmap); + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void preCheck() { + // metrics is null + assertThrows(IllegalArgumentException.class, () -> { + snmpCollect.preCheck(null); + }); + + // snmp protocol is null + assertThrows(IllegalArgumentException.class, () -> { + Metrics metrics = new Metrics(); + snmpCollect.preCheck(metrics); + }); + + // snmp host is null + assertThrows(IllegalArgumentException.class, () -> { + Metrics metrics = new Metrics(); + metrics.setSnmp(new SnmpProtocol()); + snmpCollect.preCheck(metrics); + }); + + // snmp port is null + assertThrows(IllegalArgumentException.class, () -> { + SnmpProtocol snmap = new SnmpProtocol(); + snmap.setHost("127.0.0.1"); + Metrics metrics = new Metrics(); + metrics.setSnmp(snmap); + snmpCollect.preCheck(metrics); + }); + + // snmp version is null + assertThrows(IllegalArgumentException.class, () -> { + SnmpProtocol snmap = new SnmpProtocol(); + snmap.setHost("127.0.0.1"); + snmap.setPort("161"); + Metrics metrics = new Metrics(); + metrics.setSnmp(snmap); + snmpCollect.preCheck(metrics); + }); + + // everything is ok + assertDoesNotThrow(() -> { + snmpCollect.preCheck(metrics); + }); + } + + @Test + void collect() { + assertDoesNotThrow(() -> { + snmpCollect.collect(builder, metrics); + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + }); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_SNMP, snmpCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ssh/SshCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ssh/SshCollectImplTest.java new file mode 100644 index 0000000..2810bb3 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/ssh/SshCollectImplTest.java @@ -0,0 +1,244 @@ +/* + * 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.collector.collect.ssh; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.List; +import org.apache.hertzbeat.collector.collect.common.ssh.SshHelper; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.SshProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.sshd.client.channel.ChannelExec; +import org.apache.sshd.client.channel.ClientChannel; +import org.apache.sshd.client.future.OpenFuture; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.future.CloseFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +/** + * Test case for {@link SshCollectImpl} + */ +class SshCollectImplTest { + private SshCollectImpl sshCollect; + private CollectRep.MetricsData.Builder builder; + + @BeforeEach + void setUp() { + sshCollect = new SshCollectImpl(); + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void preCheck() { + // metrics is null + assertThrows(IllegalArgumentException.class, () -> { + sshCollect.preCheck(null); + }); + + // ssh protocol is null + assertThrows(IllegalArgumentException.class, () -> { + Metrics metrics = Metrics.builder().build(); + sshCollect.preCheck(metrics); + }); + + // everything is ok + assertDoesNotThrow(() -> { + Metrics metrics = Metrics.builder().ssh(new SshProtocol()).build(); + sshCollect.preCheck(metrics); + }); + } + + @Test + void collect() { + assertDoesNotThrow(() -> { + Metrics metrics = Metrics.builder().ssh(new SshProtocol()).build(); + sshCollect.collect(builder, metrics); + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + }); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_SSH, sshCollect.supportProtocol()); + } + + @Test + void closeChannelWaitsForGracefulClose() throws IOException { + ClientChannel channel = mock(ClientChannel.class); + CloseFuture closeFuture = mock(CloseFuture.class); + when(channel.close(false)).thenReturn(closeFuture); + when(closeFuture.await(1_000)).thenReturn(true); + + assertTrue(SshCollectImpl.closeChannel(channel, 1_000)); + verify(channel).close(false); + verify(channel, never()).close(true); + } + + @Test + void closeChannelForcesCleanupAfterGracefulTimeout() throws IOException { + ClientChannel channel = mock(ClientChannel.class); + CloseFuture gracefulClose = mock(CloseFuture.class); + CloseFuture immediateClose = mock(CloseFuture.class); + when(channel.close(false)).thenReturn(gracefulClose); + when(gracefulClose.await(1_000)).thenReturn(false); + when(channel.close(true)).thenReturn(immediateClose); + when(immediateClose.await(1_000)).thenReturn(true); + + assertTrue(SshCollectImpl.closeChannel(channel, 1_000)); + verify(channel).close(false); + verify(channel).close(true); + } + + @Test + void closeChannelForcesCleanupAndPreservesInterrupt() throws IOException { + ClientChannel channel = mock(ClientChannel.class); + CloseFuture gracefulClose = mock(CloseFuture.class); + when(channel.close(false)).thenReturn(gracefulClose); + when(gracefulClose.await(1_000)).thenThrow(new InterruptedIOException("interrupted")); + + try { + assertThrows(InterruptedIOException.class, + () -> SshCollectImpl.closeChannel(channel, 1_000)); + assertTrue(Thread.currentThread().isInterrupted()); + verify(channel).close(true); + } finally { + Thread.interrupted(); + } + } + + @Test + void closeChannelSkipsAlreadyClosedChannel() throws IOException { + ClientChannel channel = mock(ClientChannel.class); + when(channel.isClosed()).thenReturn(true); + + assertTrue(SshCollectImpl.closeChannel(channel, 1_000)); + verify(channel, never()).close(false); + verify(channel, never()).close(true); + } + + @Test + void closeChannelSuppressesForcedCloseFailureAndPreservesInterrupt() throws IOException { + ClientChannel channel = mock(ClientChannel.class); + CloseFuture gracefulClose = mock(CloseFuture.class); + InterruptedIOException interrupted = new InterruptedIOException("interrupted"); + IllegalStateException closeFailure = new IllegalStateException("force close failed"); + when(channel.close(false)).thenReturn(gracefulClose); + when(gracefulClose.await(1_000)).thenThrow(interrupted); + when(channel.close(true)).thenThrow(closeFailure); + + try { + InterruptedIOException thrown = assertThrows(InterruptedIOException.class, + () -> SshCollectImpl.closeChannel(channel, 1_000)); + assertSame(interrupted, thrown); + assertEquals(List.of(closeFailure), List.of(thrown.getSuppressed())); + assertTrue(Thread.currentThread().isInterrupted()); + } finally { + Thread.interrupted(); + } + } + + @Test + void collectFinallyAwaitsGracefulChannelClose() throws Exception { + int timeout = 1_000; + SshProtocol protocol = protocol(timeout); + Metrics metrics = Metrics.builder().ssh(protocol).build(); + ClientSession clientSession = mock(ClientSession.class); + ChannelExec channel = mock(ChannelExec.class); + OpenFuture openFuture = mock(OpenFuture.class); + CloseFuture closeFuture = mock(CloseFuture.class); + when(clientSession.createExecChannel("echo ok")).thenReturn(channel); + when(channel.open()).thenReturn(openFuture); + when(openFuture.verify(timeout)).thenThrow(new SshException("channel open failed")); + when(channel.close(false)).thenReturn(closeFuture); + when(closeFuture.await(timeout)).thenReturn(true); + + try (MockedStatic<SshHelper> sshHelper = mockStatic(SshHelper.class)) { + sshHelper.when(() -> SshHelper.getConnectSession(protocol, timeout, true, false)) + .thenReturn(clientSession); + + sshCollect.collect(builder, metrics); + } + + assertEquals(CollectRep.Code.UN_CONNECTABLE, builder.getCode()); + verify(channel).close(false); + verify(closeFuture).await(timeout); + verify(channel, never()).close(true); + verify(clientSession, never()).close(); + } + + @Test + void collectClosesExactSessionWhenChannelCannotClose() throws Exception { + int timeout = 1_000; + SshProtocol protocol = protocol(timeout); + Metrics metrics = Metrics.builder().ssh(protocol).build(); + ClientSession clientSession = mock(ClientSession.class); + ChannelExec channel = mock(ChannelExec.class); + OpenFuture openFuture = mock(OpenFuture.class); + CloseFuture gracefulClose = mock(CloseFuture.class); + CloseFuture immediateClose = mock(CloseFuture.class); + when(clientSession.createExecChannel("echo ok")).thenReturn(channel); + when(channel.open()).thenReturn(openFuture); + when(openFuture.verify(timeout)).thenThrow(new SshException("channel open failed")); + when(channel.close(false)).thenReturn(gracefulClose); + when(gracefulClose.await(timeout)).thenReturn(false); + when(channel.close(true)).thenReturn(immediateClose); + when(immediateClose.await(timeout)).thenReturn(false); + + try (MockedStatic<SshHelper> sshHelper = mockStatic(SshHelper.class)) { + sshHelper.when(() -> SshHelper.getConnectSession(protocol, timeout, true, false)) + .thenReturn(clientSession); + + sshCollect.collect(builder, metrics); + } + + verify(channel).close(false); + verify(gracefulClose).await(timeout); + verify(channel).close(true); + verify(immediateClose).await(timeout); + verify(clientSession).close(); + } + + private SshProtocol protocol(int timeout) { + return SshProtocol.builder() + .host("target.example.com") + .port("22") + .username("root") + .password("password") + .timeout(String.valueOf(timeout)) + .reuseConnection("true") + .useProxy("false") + .script("echo ok") + .build(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImplTest.java new file mode 100644 index 0000000..0c1b724 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/telnet/TelnetCollectImplTest.java @@ -0,0 +1,181 @@ +/* + * 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.collector.collect.telnet; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.net.telnet.TelnetClient; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.TelnetProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link TelnetCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +class TelnetCollectImplTest { + + @InjectMocks + private TelnetCollectImpl telnetCollect; + + @Test + void testCollectWithEquals() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + TelnetProtocol telnetProtocol = TelnetProtocol.builder() + .timeout("10") + .port("21") + .cmd("ls") + .build(); + + String httpResponse = """ + a=SomeValue + b=AnotherValue + c=YetAnotherValue"""; + byte[] responseBytes = httpResponse.getBytes(StandardCharsets.UTF_8); + InputStream inputStream = new ByteArrayInputStream(responseBytes); + + MockedConstruction<TelnetClient> mocked = + Mockito.mockConstruction(TelnetClient.class, (telnetClient, context) -> { + Mockito.doNothing().when(telnetClient).connect(telnetProtocol.getHost(), + Integer.parseInt(telnetProtocol.getPort())); + Mockito.doNothing().when(telnetClient).disconnect(); + Mockito.when(telnetClient.isConnected()).thenReturn(true); + + OutputStream out = Mockito.mock(OutputStream.class); + Mockito.when(telnetClient.getOutputStream()).thenReturn(out); + Mockito.doNothing().when(out).write(Mockito.any()); + Mockito.doNothing().when(out).flush(); + + Mockito.when(telnetClient.getInputStream()).thenReturn(inputStream); + + }); + + + List<String> aliasField = new ArrayList<>(); + aliasField.add("responseTime"); + aliasField.add("a"); + aliasField.add("b"); + aliasField.add("c"); + Metrics metrics = new Metrics(); + metrics.setTelnet(telnetProtocol); + metrics.setAliasFields(aliasField); + telnetCollect.preCheck(metrics); + telnetCollect.collect(builder, metrics); + assertEquals(builder.getValuesCount(), 1); + for (CollectRep.ValueRow valueRow : builder.getValuesList()) { + assertNotNull(valueRow.getColumns(0)); + assertEquals(valueRow.getColumns(1), "SomeValue"); + assertEquals(valueRow.getColumns(2), "AnotherValue"); + assertEquals(valueRow.getColumns(3), "YetAnotherValue"); + } + mocked.close(); + } + + @Test + void testCollectWithTab() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + TelnetProtocol telnetProtocol = TelnetProtocol.builder() + .timeout("10") + .port("21") + .cmd("ls") + .build(); + + String httpResponse = """ + a\tSomeValue + b\tAnotherValue + c\tYetAnotherValue"""; + byte[] responseBytes = httpResponse.getBytes(StandardCharsets.UTF_8); + InputStream inputStream = new ByteArrayInputStream(responseBytes); + + MockedConstruction<TelnetClient> mocked = + Mockito.mockConstruction(TelnetClient.class, (telnetClient, context) -> { + Mockito.doNothing().when(telnetClient).connect(telnetProtocol.getHost(), + Integer.parseInt(telnetProtocol.getPort())); + Mockito.doNothing().when(telnetClient).disconnect(); + Mockito.when(telnetClient.isConnected()).thenReturn(true); + + OutputStream out = Mockito.mock(OutputStream.class); + Mockito.when(telnetClient.getOutputStream()).thenReturn(out); + Mockito.doNothing().when(out).write(Mockito.any()); + Mockito.doNothing().when(out).flush(); + + Mockito.when(telnetClient.getInputStream()).thenReturn(inputStream); + + }); + + + List<String> aliasField = new ArrayList<>(); + aliasField.add("responseTime"); + aliasField.add("a"); + aliasField.add("b"); + aliasField.add("c"); + Metrics metrics = new Metrics(); + metrics.setTelnet(telnetProtocol); + metrics.setAliasFields(aliasField); + telnetCollect.preCheck(metrics); + telnetCollect.collect(builder, metrics); + assertEquals(builder.getValuesCount(), 1); + for (CollectRep.ValueRow valueRow : builder.getValuesList()) { + assertNotNull(valueRow.getColumns(0)); + assertEquals(valueRow.getColumns(1), "SomeValue"); + assertEquals(valueRow.getColumns(2), "AnotherValue"); + assertEquals(valueRow.getColumns(3), "YetAnotherValue"); + } + mocked.close(); + } + + @Test + void preCheck() throws IllegalArgumentException { + // metrics is null + assertThrows(IllegalArgumentException.class, () -> telnetCollect.preCheck(null)); + + // protocol is null + assertThrows(IllegalArgumentException.class, () -> { + Metrics metrics = new Metrics(); + telnetCollect.preCheck(metrics); + }); + + // everyting is ok + assertDoesNotThrow(() -> { + Metrics metrics = new Metrics(); + metrics.setTelnet(TelnetProtocol.builder().build()); + telnetCollect.preCheck(metrics); + }); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_TELNET, telnetCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/udp/UdpCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/udp/UdpCollectImplTest.java new file mode 100644 index 0000000..579974e --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/udp/UdpCollectImplTest.java @@ -0,0 +1,158 @@ +/* + * 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.collector.collect.udp; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.PortUnreachableException; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.UdpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link UdpCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +class UdpCollectImplTest { + + @InjectMocks + private UdpCollectImpl udpCollect; + + @Test + void testPreCheck() { + List<String> aliasField = new ArrayList<>(); + aliasField.add("responseTime"); + Metrics metrics = new Metrics(); + metrics.setAliasFields(aliasField); + assertThrows(IllegalArgumentException.class, () -> udpCollect.preCheck(metrics)); + + // metrics is null + assertThrows(IllegalArgumentException.class, () -> { + udpCollect.preCheck(null); + }); + + // everything is ok + assertDoesNotThrow(() -> { + UdpProtocol udpProtocol = UdpProtocol.builder().timeout("10").port("21").host("127.0.0.1").build(); + udpCollect.preCheck(Metrics.builder().udp(udpProtocol).build()); + }); + } + + @Test + void testCollect() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + UdpProtocol udpProtocol = UdpProtocol.builder() + .timeout("10") + .port("21") + .host("127.0.0.1") + .build(); + + MockedConstruction<DatagramSocket> socketMockedConstruction = + Mockito.mockConstruction(DatagramSocket.class, (socket, context) -> { + Mockito.doNothing().when(socket).send(Mockito.any(DatagramPacket.class)); + Mockito.doNothing().when(socket).receive(Mockito.any(DatagramPacket.class)); + }); + + + List<String> aliasField = new ArrayList<>(); + aliasField.add("responseTime"); + Metrics metrics = new Metrics(); + metrics.setUdp(udpProtocol); + metrics.setAliasFields(aliasField); + udpCollect.preCheck(metrics); + udpCollect.collect(builder, metrics); + assertEquals(builder.getValuesCount(), 1); + for (CollectRep.ValueRow valueRow : builder.getValuesList()) { + assertNotNull(valueRow.getColumns(0)); + } + + socketMockedConstruction.close(); + } + + @Test + void testCollectWithSocketException() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + UdpProtocol udpProtocol = UdpProtocol.builder() + .timeout("10") + .port("21") + .host("127.0.0.1") + .build(); + + MockedConstruction<DatagramSocket> socketMockedConstruction = + Mockito.mockConstruction(DatagramSocket.class, (socket, context) -> Mockito.doThrow(new SocketTimeoutException("test exception")) + .when(socket).send(Mockito.any(DatagramPacket.class))); + + + List<String> aliasField = new ArrayList<>(); + aliasField.add("responseTime"); + Metrics metrics = new Metrics(); + metrics.setUdp(udpProtocol); + metrics.setAliasFields(aliasField); + udpCollect.preCheck(metrics); + udpCollect.collect(builder, metrics); + assertEquals(builder.getCode(), CollectRep.Code.UN_CONNECTABLE); + + socketMockedConstruction.close(); + } + + @Test + void testCollectWithPortUnreachableException() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + UdpProtocol udpProtocol = UdpProtocol.builder() + .timeout("10") + .port("21") + .host("127.0.0.1") + .build(); + + MockedConstruction<DatagramSocket> socketMockedConstruction = + Mockito.mockConstruction(DatagramSocket.class, (socket, context) -> Mockito.doThrow(new PortUnreachableException("test exception")) + .when(socket).send(Mockito.any(DatagramPacket.class))); + + + List<String> aliasField = new ArrayList<>(); + aliasField.add("responseTime"); + Metrics metrics = new Metrics(); + metrics.setUdp(udpProtocol); + metrics.setAliasFields(aliasField); + udpCollect.preCheck(metrics); + udpCollect.collect(builder, metrics); + assertEquals(builder.getCode(), CollectRep.Code.UN_REACHABLE); + + socketMockedConstruction.close(); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_UDP, udpCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/websocket/WebsocketCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/websocket/WebsocketCollectImplTest.java new file mode 100644 index 0000000..ab93602 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/websocket/WebsocketCollectImplTest.java @@ -0,0 +1,101 @@ +/* + * 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.collector.collect.websocket; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.WebsocketProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link WebsocketCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +class WebsocketCollectImplTest { + + @InjectMocks + private WebsocketCollectImpl websocketCollectImpl; + + @Test + void testCollect() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + WebsocketProtocol websocketProtocol = WebsocketProtocol.builder() + .host("127.0.0.1") + .path("/") + .port("80") + .build(); + + String httpResponse = """ + HTTP/1.1 200 OK\r + Content-Type: text/html; charset=UTF-8\r + Content-Length: 1234\r + Date: Sat, 4 May 2024 12:00:00 GMT\r + Connection: close\r + \r + """; + byte[] responseBytes = httpResponse.getBytes(StandardCharsets.UTF_8); + InputStream inputStream = new ByteArrayInputStream(responseBytes); + + MockedConstruction<Socket> socketMockedConstruction = + Mockito.mockConstruction(Socket.class, (socket, context) -> { + OutputStream out = Mockito.mock(OutputStream.class); + Mockito.doNothing().when(socket).connect(Mockito.any(SocketAddress.class)); + Mockito.when(socket.isConnected()).thenReturn(true); + Mockito.when(socket.getOutputStream()).thenReturn(out); + Mockito.doNothing().when(out).write(Mockito.any()); + Mockito.doNothing().when(out).flush(); + Mockito.when(socket.getInputStream()).thenReturn(inputStream); + + }); + + + List<String> aliasField = new ArrayList<>(); + aliasField.add("httpVersion"); + aliasField.add("responseTime"); + aliasField.add("responseCode"); + Metrics metrics = new Metrics(); + metrics.setWebsocket(websocketProtocol); + metrics.setAliasFields(aliasField); + websocketCollectImpl.preCheck(metrics); + websocketCollectImpl.collect(builder, metrics); + assertEquals(builder.getValuesCount(), 1); + for (CollectRep.ValueRow valueRow : builder.getValuesList()) { + assertEquals(valueRow.getColumns(0), "HTTP/1.1"); + assertNotNull(valueRow.getColumns(1)); + assertEquals(valueRow.getColumns(2), "200"); + } + + socketMockedConstruction.close(); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/WorkerPoolTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/WorkerPoolTest.java new file mode 100644 index 0000000..2348dc1 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/WorkerPoolTest.java @@ -0,0 +1,91 @@ +/* + * 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.collector.dispatch; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.hertzbeat.common.concurrent.AdmissionMode; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link WorkerPool} + */ +class WorkerPoolTest { + + private WorkerPool workerPool; + + @AfterEach + void tearDown() throws Exception { + if (workerPool != null) { + workerPool.destroy(); + } + } + + @Test + void testExecuteJobRunsOnVirtualThread() throws Exception { + workerPool = new WorkerPool(); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + + workerPool.executeJob(() -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void testExecuteJobRejectsWhenConcurrencyLimitReached() throws Exception { + VirtualThreadProperties properties = new VirtualThreadProperties( + true, + new VirtualThreadProperties.PoolProperties(AdmissionMode.LIMIT_AND_REJECT, 1), + VirtualThreadProperties.PoolProperties.commonDefaults(), + VirtualThreadProperties.PoolProperties.managerDefaults(), + VirtualThreadProperties.AlerterProperties.defaults(), + VirtualThreadProperties.PoolProperties.warehouseDefaults(), + VirtualThreadProperties.AsyncProperties.defaults()); + workerPool = new WorkerPool(properties); + + CountDownLatch started = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + workerPool.executeJob(() -> { + started.countDown(); + try { + release.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertTrue(started.await(5, TimeUnit.SECONDS)); + + try { + assertThrows(RejectedExecutionException.class, () -> workerPool.executeJob(() -> { + })); + } finally { + release.countDown(); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/entrance/CollectServerTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/entrance/CollectServerTest.java new file mode 100644 index 0000000..03a6977 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/entrance/CollectServerTest.java @@ -0,0 +1,218 @@ +/* + * 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.collector.dispatch.entrance; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import io.netty.channel.Channel; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.hertzbeat.collector.dispatch.CollectorInfoProperties; +import org.apache.hertzbeat.collector.dispatch.DispatchProperties; +import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService; +import org.apache.hertzbeat.collector.timer.TimerDispatch; +import org.apache.hertzbeat.common.concurrent.BackgroundTaskExecutor; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.remoting.RemotingClient; +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.test.util.ReflectionTestUtils; + +/** + * test case for {@link CollectServer} + */ + +@ExtendWith(MockitoExtension.class) +class CollectServerTest { + + @Mock + private CollectJobService collectJobService; + + @Mock + private TimerDispatch timerDispatch; + + @Mock + private DispatchProperties properties; + + @Mock + private DispatchProperties.EntranceProperties entranceProperties; + + @Mock + private DispatchProperties.EntranceProperties.NettyProperties nettyProperties; + + @Mock + private BackgroundTaskExecutor threadPool; + + @Mock + private CollectorInfoProperties infoProperties; + + private CollectServer collectServer; + + private CollectServer.CollectNettyEventListener collectNettyEventListener; + + @BeforeEach + void setUp() { + + when(nettyProperties.getManagerHost()).thenReturn("127.0.0.1"); + when(nettyProperties.getManagerPort()).thenReturn(8080); + when(entranceProperties.getNetty()).thenReturn(nettyProperties); + when(properties.getEntrance()).thenReturn(entranceProperties); + + collectServer = new CollectServer(collectJobService, timerDispatch, properties, threadPool, infoProperties); + collectNettyEventListener = collectServer.new CollectNettyEventListener(); + } + + @Test + void testRun() throws Exception { + + RemotingClient remotingClient = mock(RemotingClient.class); + ReflectionTestUtils.setField(collectServer, "remotingClient", remotingClient); + + collectServer.run(); + + verify(remotingClient, times(1)).start(); + } + + @Test + void testShutdown() { + + RemotingClient remotingClient = mock(RemotingClient.class); + ReflectionTestUtils.setField(collectServer, "remotingClient", remotingClient); + ReflectionTestUtils.setField(collectServer, "scheduledExecutor", mock(ScheduledExecutorService.class)); + + collectServer.shutdown(); + + ScheduledExecutorService scheduledExecutor = (ScheduledExecutorService) ReflectionTestUtils.getField(collectServer, "scheduledExecutor"); + verify((scheduledExecutor), times(1)).shutdownNow(); + verify(remotingClient, times(1)).shutdown(); + } + + @Test + void testSendMsg() { + + RemotingClient remotingClient = mock(RemotingClient.class); + ReflectionTestUtils.setField(collectServer, "remotingClient", remotingClient); + ClusterMsg.Message message = mock(ClusterMsg.Message.class); + + collectServer.sendMsg(message); + + verify(remotingClient, times(1)).sendMsg(message); + } + + @Test + void testOnChannelActive() { + + RemotingClient remotingClient = mock(RemotingClient.class); + ReflectionTestUtils.setField(collectServer, "remotingClient", remotingClient); + + Channel channel = mock(Channel.class); + when(collectJobService.getCollectorIdentity()).thenReturn("collector1"); + when(collectJobService.getCollectorMode()).thenReturn("mode1"); + when(infoProperties.getIp()).thenReturn("127.0.0.1"); + when(infoProperties.getVersion()).thenReturn("1.0"); + + collectNettyEventListener.onChannelActive(channel); + + verify(timerDispatch, times(1)).goOnline(); + verify(remotingClient, times(1)).sendMsg(any(ClusterMsg.Message.class)); + + ScheduledExecutorService scheduledExecutor = + (ScheduledExecutorService) ReflectionTestUtils.getField(collectServer, "scheduledExecutor"); + assertNotNull(scheduledExecutor); + } + + @Test + void testDispatchHeartbeatRunsOnVirtualThread() throws Exception { + VirtualThreadProperties properties = new VirtualThreadProperties(); + collectServer = new CollectServer(collectJobService, timerDispatch, properties(), threadPool, infoProperties, properties); + + RemotingClient remotingClient = mock(RemotingClient.class); + ReflectionTestUtils.setField(collectServer, "remotingClient", remotingClient); + + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + org.mockito.Mockito.doAnswer(invocation -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + return null; + }).when(remotingClient).sendMsg(any(ClusterMsg.Message.class)); + + collectServer.dispatchHeartbeat("collector1"); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void testDispatchHeartbeatDoesNotRunConcurrently() throws Exception { + VirtualThreadProperties properties = new VirtualThreadProperties(); + collectServer = new CollectServer(collectJobService, timerDispatch, properties(), threadPool, infoProperties, properties); + + RemotingClient remotingClient = mock(RemotingClient.class); + ReflectionTestUtils.setField(collectServer, "remotingClient", remotingClient); + + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + AtomicInteger concurrent = new AtomicInteger(); + AtomicInteger maxConcurrent = new AtomicInteger(); + AtomicInteger invocations = new AtomicInteger(); + org.mockito.Mockito.doAnswer(invocation -> { + int running = concurrent.incrementAndGet(); + maxConcurrent.accumulateAndGet(running, Math::max); + int currentInvocation = invocations.incrementAndGet(); + if (currentInvocation == 1) { + firstStarted.countDown(); + releaseFirst.await(5, TimeUnit.SECONDS); + } else if (currentInvocation == 2) { + secondStarted.countDown(); + } + concurrent.decrementAndGet(); + return null; + }).when(remotingClient).sendMsg(any(ClusterMsg.Message.class)); + + collectServer.dispatchHeartbeat("collector1"); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + collectServer.dispatchHeartbeat("collector1"); + assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS)); + + releaseFirst.countDown(); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + assertEquals(1, maxConcurrent.get()); + } + + private DispatchProperties properties() { + return properties; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/entrance/internal/CollectJobServiceTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/entrance/internal/CollectJobServiceTest.java new file mode 100644 index 0000000..574103b --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/entrance/internal/CollectJobServiceTest.java @@ -0,0 +1,154 @@ +/* + * 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.collector.dispatch.entrance.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.apache.hertzbeat.collector.dispatch.DispatchProperties; +import org.apache.hertzbeat.collector.dispatch.WorkerPool; +import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer; +import org.apache.hertzbeat.collector.timer.TimerDispatch; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test case for {@link CollectJobService} + */ + +class CollectJobServiceTest { + + @Mock + private TimerDispatch timerDispatch; + + @Mock + private WorkerPool workerPool; + + @Mock + private CollectServer collectServer; + + @Mock + private DispatchProperties properties; + + @InjectMocks + private CollectJobService collectJobService; + + @BeforeEach + public void setUp() { + + MockitoAnnotations.openMocks(this); + + DispatchProperties.EntranceProperties entrance = mock(DispatchProperties.EntranceProperties.class); + DispatchProperties.EntranceProperties.NettyProperties netty = mock(DispatchProperties.EntranceProperties.NettyProperties.class); + + when(properties.getEntrance()).thenReturn(entrance); + when(entrance.getNetty()).thenReturn(netty); + when(netty.isEnabled()).thenReturn(true); + when(netty.getMode()).thenReturn("test-mode"); + when(netty.getIdentity()).thenReturn("test-collector"); + + collectJobService = spy(new CollectJobService(timerDispatch, properties, workerPool)); + collectJobService.setCollectServer(collectServer); + } + + @Test + public void testCollectSyncJobData() { + + Job job = mock(Job.class); + List<CollectRep.MetricsData> metricsDataList = List.of(CollectRep.MetricsData.newBuilder().build()); + CountDownLatch latch = new CountDownLatch(1); + + doAnswer(invocation -> { + CollectResponseEventListener listener = invocation.getArgument(1); + listener.response(metricsDataList); + latch.countDown(); + return null; + }).when(timerDispatch).addJob(any(Job.class), any(CollectResponseEventListener.class)); + + List<CollectRep.MetricsData> result = collectJobService.collectSyncJobData(job); + + assertEquals(metricsDataList, result); + } + + @Test + public void testCollectSyncOneTimeJobData() { + + Job job = mock(Job.class); + List<CollectRep.MetricsData> metricsDataList = List.of(CollectRep.MetricsData.newBuilder().build()); + + doAnswer(invocation -> { + Runnable task = invocation.getArgument(0); + task.run(); + return null; + }).when(workerPool).executeJob(any(Runnable.class)); + + doReturn(metricsDataList).when(collectJobService).collectSyncJobData(any(Job.class)); + + collectJobService.collectSyncOneTimeJobData(job); + + verify(collectServer, times(1)).sendMsg(any(ClusterMsg.Message.class)); + } + + @Test + public void testCancelAsyncCollectJob() { + + Long jobId = 123L; + collectJobService.cancelAsyncCollectJob(jobId); + + verify(timerDispatch, times(1)).deleteJob(eq(jobId), eq(true)); + } + + @Test + public void testSendAsyncCollectData() { + + CollectRep.MetricsData metricsData = CollectRep.MetricsData + .newBuilder() + .setMetrics("test") + .build(); + collectJobService.sendAsyncCollectData(metricsData); + + verify(collectServer, times(1)).sendMsg(any(ClusterMsg.Message.class)); + } + + @Test + public void testGetCollectorIdentity() { + + assertEquals("test-collector", collectJobService.getCollectorIdentity()); + } + + @Test + public void testGetCollectorMode() { + + assertEquals("test-mode", collectJobService.getCollectorMode()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/export/NettyDataQueueTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/export/NettyDataQueueTest.java new file mode 100644 index 0000000..9c2318a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/export/NettyDataQueueTest.java @@ -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.collector.dispatch.export; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * test for {@link NettyDataQueue} + */ + +class NettyDataQueueTest { + + @Mock + private CollectJobService collectJobService; + + @InjectMocks + private NettyDataQueue nettyDataQueue; + + @BeforeEach + public void setUp() { + + MockitoAnnotations.openMocks(this); + } + + @Test + void testSendMetricsData() { + + CollectRep.MetricsData metricsData = CollectRep.MetricsData + .newBuilder() + .setMetrics("test") + .build(); + nettyDataQueue.sendMetricsData(metricsData); + + verify(collectJobService, times(1)).sendAsyncCollectData(metricsData); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/unit/DataSizeConvertTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/unit/DataSizeConvertTest.java new file mode 100644 index 0000000..5bfe3a6 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/unit/DataSizeConvertTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.dispatch.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.apache.hertzbeat.collector.dispatch.unit.impl.DataSizeConvert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link DataSizeConvert} + */ + +class DataSizeConvertTest { + + private DataSizeConvert convert; + + @BeforeEach + void setUp() { + this.convert = new DataSizeConvert(); + } + + @Test + void testConvert() { + + assertEquals("1", convert.convert("1024", "B", "KB")); + assertEquals("1024", convert.convert("1024", "B", "B")); + assertEquals("1", convert.convert("1024", "B", "kb")); + assertEquals("1", convert.convert("1024", "b", "kb")); + assertEquals("1", convert.convert("1024", "KB", "MB")); + assertEquals("1", convert.convert("1024", "MB", "GB")); + assertNull(convert.convert("", "B", "KB")); + assertNull(convert.convert("1024", "INVALID", "KB")); + assertNull(convert.convert("1024", "B", "INVALID")); + + } + + @Test + void testCheckUnit() { + assertTrue(convert.checkUnit("KB")); + assertTrue(convert.checkUnit("kb")); + assertTrue(convert.checkUnit("Kb")); + assertTrue(convert.checkUnit("kB")); + assertTrue(convert.checkUnit("MB")); + assertTrue(convert.checkUnit("GB")); + assertFalse(convert.checkUnit("INVALID")); + assertFalse(convert.checkUnit(null)); + assertFalse(convert.checkUnit("")); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/unit/TimeLengthConvertTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/unit/TimeLengthConvertTest.java new file mode 100644 index 0000000..71f2c9c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/dispatch/unit/TimeLengthConvertTest.java @@ -0,0 +1,73 @@ +/* + * 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.collector.dispatch.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.apache.hertzbeat.collector.dispatch.unit.impl.TimeLengthConvert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link TimeLengthConvert} + */ +class TimeLengthConvertTest { + + private TimeLengthConvert convert; + + @BeforeEach + void setUp() { + this.convert = new TimeLengthConvert(); + } + + /** + * test nanoseconds to second + */ + @Test + void convertNs2Sec() { + String result = convert.convert("1000000000", TimeLengthUnit.NS.getUnit(), TimeLengthUnit.S.getUnit()); + assertEquals("1", result); + } + + /** + * test nanoseconds to milliseconds + */ + @Test + void convertNs2Ms() { + String result = convert.convert("1000123450", TimeLengthUnit.NS.getUnit(), TimeLengthUnit.MS.getUnit()); + assertEquals("1000.1235", result); + } + + /** + * test nanosecond to microsecond + */ + @Test + void convertNs2Us() { + String result = convert.convert("1000000000", TimeLengthUnit.NS.getUnit(), TimeLengthUnit.US.getUnit()); + assertEquals("1000000", result); + } + + /** + * test nanoseconds to sky + */ + @Test + void convertNs2Day() { + String result = convert.convert("86400000000000", TimeLengthUnit.NS.getUnit(), TimeLengthUnit.D.getUnit()); + assertEquals("1", result); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/util/CollectUtilTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/util/CollectUtilTest.java new file mode 100644 index 0000000..e42c837 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/util/CollectUtilTest.java @@ -0,0 +1,226 @@ +/* + * 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.collector.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test case for {@link CollectUtil} + */ +class CollectUtilTest { + + @Test + void countMatchKeyword() { + int keyword = CollectUtil.countMatchKeyword("hertzbeat", "e"); + assertEquals(2, keyword); + int i = CollectUtil.countMatchKeyword("查询id183", "\\d"); + assertEquals(3, i); + assertEquals(0, CollectUtil.countMatchKeyword(null, null)); + } + + @Test + void getTimeout() { + int timeout1 = CollectUtil.getTimeout("4000"); + assertEquals(4000, timeout1); + int timeout2 = CollectUtil.getTimeout("4000.5"); + assertEquals(4000, timeout2); + int timeout3 = CollectUtil.getTimeout("a4000.5"); + assertEquals(60000, timeout3); + int timeout4 = CollectUtil.getTimeout(null); + assertEquals(60000, timeout4); + } + + @Test + void extractDoubleAndUnitFromStr() { + CollectUtil.DoubleAndUnit res1 = CollectUtil.extractDoubleAndUnitFromStr("20.5%"); + assertNotNull(res1); + assertEquals(20.5, res1.getValue()); + assertEquals("%", res1.getUnit()); + + CollectUtil.DoubleAndUnit res2 = CollectUtil.extractDoubleAndUnitFromStr(""); + assertNull(res2); + + CollectUtil.DoubleAndUnit res3 = CollectUtil.extractDoubleAndUnitFromStr("KB"); + assertNotNull(res3); + assertEquals(0, res3.getValue()); + assertEquals("KB", res3.getUnit()); + + CollectUtil.DoubleAndUnit res4 = CollectUtil.extractDoubleAndUnitFromStr("GRAPH0"); + assertNull(res4.getValue()); + assertNull(res4.getUnit()); + + CollectUtil.DoubleAndUnit res5 = CollectUtil.extractDoubleAndUnitFromStr("200Ki"); + assertEquals(200, res5.getValue()); + assertEquals("Ki", res5.getUnit()); + CollectUtil.DoubleAndUnit res6 = CollectUtil.extractDoubleAndUnitFromStr("200Mi"); + assertEquals(200, res6.getValue()); + assertEquals("Mi", res6.getUnit()); + CollectUtil.DoubleAndUnit res7 = CollectUtil.extractDoubleAndUnitFromStr("200Gi"); + assertEquals(200, res7.getValue()); + assertEquals("Gi", res7.getUnit()); + } + + + @Test + void assertPromRequireField() { + assertEquals(true, CollectUtil.assertPromRequireField("timestamp")); + assertEquals(true, CollectUtil.assertPromRequireField("value")); + assertEquals(false, CollectUtil.assertPromRequireField("timestamp_value")); + } + + @Test + void containCryPlaceholder() { + Metrics metrics = Metrics.builder().name("^o^name^o^").build(); + JsonElement jsonElement = new Gson().toJsonTree(metrics); + assertTrue(CollectUtil.containCryPlaceholder(jsonElement)); + } + + @Test + void replaceCryPlaceholder() { + Metrics metrics = Metrics.builder().name("^o^name^o^").build(); + JsonElement jsonElement = new Gson().toJsonTree(metrics); + Map<String, Configmap> configmap = new HashMap<>(); + Configmap config = Configmap.builder().key("name").value("张三").build(); + configmap.put("name", config); + JsonElement res1 = CollectUtil.replaceCryPlaceholder(jsonElement, configmap); + + Metrics metricsTarget = Metrics.builder().name("张三").build(); + JsonElement jsonElementTarget = new Gson().toJsonTree(metricsTarget); + assertEquals(JsonUtil.fromJson(jsonElementTarget.toString()), JsonUtil.fromJson(res1.toString())); + + List<Metrics> metricsList = new ArrayList<>(); + metricsList.add(metrics); + metricsList.add(metrics); + JsonElement jsonArray = new Gson().toJsonTree(metricsList); + JsonElement res2 = CollectUtil.replaceCryPlaceholder(jsonArray, configmap); + + List<Metrics> metricsListTarget = new ArrayList<>(); + metricsListTarget.add(metricsTarget); + metricsListTarget.add(metricsTarget); + JsonElement jsonArrayTarget = new Gson().toJsonTree(metricsListTarget); + assertEquals(JsonUtil.fromJson(jsonArrayTarget.toString()), JsonUtil.fromJson(res2.toString())); + } + + static Stream<Arguments> testParamsForShouldVerifyReplaceCryPlaceholder() { + JsonObject jsonObject = new JsonObject(); + String value = "^o^A1-B2.C3^o^"; + String replacedField = "A1-B2.C3"; + String replacedValue = "A1B2C3"; + String nameKey = "name"; + String messageKey = "message"; + + jsonObject.add(messageKey, new JsonPrimitive(value)); + Map<String, Configmap> configmap = new HashMap<>(); + Configmap config = Configmap.builder().key(nameKey).value(replacedValue).build(); + configmap.put(replacedField, config); + + JsonObject jsonObjectExpected = new JsonObject(); + jsonObjectExpected.addProperty(messageKey, replacedValue); + + Map<String, Configmap> configmapUnmatched = new HashMap<>(); + Configmap configUnmatched = Configmap.builder().key(nameKey).value(replacedValue).build(); + configmapUnmatched.put(nameKey, configUnmatched); + + JsonObject jsonObjectExpectedForUnmatched = new JsonObject(); + jsonObjectExpectedForUnmatched.addProperty(messageKey, value); + + Map<String, Configmap> configMapSameLength = new HashMap<>(); + Configmap configSameLength = Configmap.builder().key(nameKey).value(null).build(); + configMapSameLength.put(replacedField, configSameLength); + + JsonObject jsonObjectExpectedForSameLength = new JsonObject(); + jsonObjectExpectedForSameLength.addProperty(messageKey, (String) null); + + return Stream.of( + Arguments.of(jsonObject.deepCopy(), configMapSameLength, jsonObjectExpectedForSameLength), + Arguments.of(jsonObject.deepCopy(), configmap, jsonObjectExpected), + Arguments.of(jsonObject.deepCopy(), configmapUnmatched, jsonObjectExpectedForUnmatched) + ); + } + + @ParameterizedTest + @MethodSource("testParamsForShouldVerifyReplaceCryPlaceholder") + void shouldVerifyReplaceCryPlaceholder(JsonObject jsonObject, + Map<String, Configmap> configmap, + JsonObject jsonObjectTarget) { + + JsonElement res1 = CollectUtil.replaceCryPlaceholder(jsonObject, configmap); + assertEquals(JsonUtil.fromJson(jsonObjectTarget.toString()), JsonUtil.fromJson(res1.toString())); + + List<JsonObject> metricsList = new ArrayList<>(); + metricsList.add(jsonObject); + JsonElement jsonArray = new Gson().toJsonTree(metricsList); + JsonElement res2 = CollectUtil.replaceCryPlaceholder(jsonArray, configmap); + + List<JsonObject> metricsListTarget = new ArrayList<>(); + + metricsListTarget.add(jsonObjectTarget); + JsonElement jsonArrayTarget = new Gson().toJsonTree(metricsListTarget); + assertEquals(JsonUtil.fromJson(jsonArrayTarget.toString()), JsonUtil.fromJson(res2.toString())); + } + + @Test + void replaceSmilingPlaceholder() { + Metrics metrics = Metrics.builder().name("^_^name^_^").build(); + JsonElement jsonElement = new Gson().toJsonTree(metrics); + HashMap<String, Configmap> configmap = new HashMap<>(); + Configmap config = Configmap.builder().key("name").value("张三").build(); + configmap.put("name", config); + JsonElement res = CollectUtil.replaceSmilingPlaceholder(jsonElement, configmap); + Metrics metricsTarget = Metrics.builder().name("张三").build(); + JsonElement jsonElement2 = new Gson().toJsonTree(metricsTarget); + assertEquals(JsonUtil.fromJson(jsonElement2.toString()), JsonUtil.fromJson(res.toString())); + + List<Metrics> metricsList = new ArrayList<>(); + metricsList.add(metrics); + metricsList.add(metrics); + JsonElement jsonArray = new Gson().toJsonTree(metricsList); + JsonElement res2 = CollectUtil.replaceSmilingPlaceholder(jsonArray, configmap); + + List<Metrics> metricsListTarget = new ArrayList<>(); + metricsListTarget.add(metricsTarget); + metricsListTarget.add(metricsTarget); + JsonElement jsonArrayTarget = new Gson().toJsonTree(metricsListTarget); + assertEquals(JsonUtil.fromJson(jsonArrayTarget.toString()), JsonUtil.fromJson(res2.toString())); + } + + @Test + void replaceUriSpecialChar() { + assertEquals("google.com%20", CollectUtil.replaceUriSpecialChar("google.com ")); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/util/JsonPathParserTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/util/JsonPathParserTest.java new file mode 100644 index 0000000..e9beb6e --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/util/JsonPathParserTest.java @@ -0,0 +1,102 @@ +/* + * 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.collector.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link JsonPathParser} + */ +class JsonPathParserTest { + + private static final String JSON_ARRAY = "[{'name': 'tom', 'speed': '433'},{'name': 'lili', 'speed': '543'}]"; + + public static final String JSON_OBJECT = """ + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95, + "gears": [23, 50], + "extra": {"x": 0}, + "escape": "Esc\\b\\f\\n\\r\\t\\u002A", + "nullValue": null + } + } + } + """; + + @Test + void parseContentWithJsonPath() { + // process array + List<Object> tom = JsonPathParser.parseContentWithJsonPath(JSON_ARRAY, "$[0].name"); + assertNotNull(tom); + assertEquals("tom", tom.get(0)); + // get json array map + List<Object> entry = JsonPathParser.parseContentWithJsonPath(JSON_ARRAY, "$[1]"); + assertNotNull(entry); + entry.forEach(e -> { + assertInstanceOf(Map.class, e); + assertEquals("543", ((Map) e).get("speed")); + }); + // process object + List<Object> author = JsonPathParser.parseContentWithJsonPath(JSON_OBJECT, "$.store.book[0].author"); + assertNotNull(author); + assertEquals("Nigel Rees", author.get(0)); + // get json object map + List<Object> book = JsonPathParser.parseContentWithJsonPath(JSON_OBJECT, "$.store.book[1]"); + assertNotNull(book); + book.forEach(e -> { + assertInstanceOf(Map.class, e); + assertEquals("Sword of Honour", ((Map) e).get("title")); + }); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/util/PrivateKeyUtilsTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/util/PrivateKeyUtilsTest.java new file mode 100644 index 0000000..68f611a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/util/PrivateKeyUtilsTest.java @@ -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.collector.util; + +import java.io.IOException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * test case for {@link PrivateKeyUtils} + */ +class PrivateKeyUtilsTest { + + @DisplayName("write key to ~/.ssh") + @Test + void writePrivateKey() throws IOException { + var key = """ + -----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEA4ctFYk/xy89L6/6YFeeMrwCW9lCP/ThXMn+9G63s5bGn4oIN + 8cEf/JYkmGw8vMP41IAP9dyH8ji2wIZSLeTPWucEK6P6jA01iIBQ95ng6RTsnQgL + h4pYHxlEaNHcXkjy5GlMdzaWadjdRevpThGR1VOtWFtK3yoC0c/te2Junu04f+11 + cpk8QvmVfzrBUooVnG0/7oekwUy1c5sSl0qVoLzXOv4XG9w34cyvacFC30zv1Nl8 + ASi2pmOBVx9njPvqQ7qZrDk0nwn+RZUmGh/PbmHxrBV7ZA5NjZcEnf2VGIfjGUVu + qE4VnkbvS4j03afV2rsp1yo74K+k/ZC6GCHB5QIBIwKCAQBG9r4I9I3SVxfcdJYy + xR2WFiDREgFeNkdKYqkl9NVsws5dIY9am8g5cQQv54DNnK1KGZ6dulaclXtD0nGZ + ZSs505OYr+EHcd2f7dBN0Uavp32QcD4jSLycD0FixZ0HsIbaEnceJxlUd1t8YBYf + 2aLcpUUbxOulORbUOgjPAa286uDeQYN5IbdruDfvbuFFm7hBoGZoKLJ7FPcJ0U3A + 14KRK+Z1oCYJIS0ubaHbhaPIVPPQEmTNHpsvxIJXfZtVy9+XIuBGmD3+Aq6SSFPC + A8mU1iKzzdRCXZwvPeUiivIIZc6DRXjhtJ2Lya/XndKidOT/QUj8Z+f9pWAonlzM + 3PMXAoGBAPvzctkkDjUJjLyEuYQq8soYokS4n4ykFTP5oFgnodK/cYocbxTT6Tn9 + vH7b6lK6ZAf+tZk8rcEeIO650pOvmaa1/OuZSxfcFUGBvOvYXiHF7zmkePh/pQgB + 7Cl0RYrI52Cjbd9aCUIYK3A82qsUq30INGeOhMNrfaHn2pgx8xlDAoGBAOVsNctw + CHnLaIQX8eS+eUcQEm+NZppnDBJavdpP48ZZM/t5v/2fQ5ytbYqk0KEzIGu0dP8g + jfB76JbMvStvTfB+TrXsfhGyA3oJrEcG+3IUshsRU2sohT1ScY27z2VMLgilnWvF + 7t49sQm9uB/yn669n8LIciHxDItOpvqgKdG3AoGBAO2NxA6PtZ+4jAIz/19bsbc7 + zDIqaovrKe8tMMglXg/ZE0e0aLvdvqRkRAKU1Z51Ob5lLuDwEYoyWZCgk1gL90Vp + wpT+P3zlcyCBo39IWMDB8C8IydRbF/GbaaNtoKds92m+qWwwUd87XCf+3M0wvvI6 + 75TW1PLEbyOgFz8Khh8hAoGBAJbDc87Ul9sCAtp2Ip2hvWk2coPR8vfADz9C8cn5 + /BShBOcVfipSt2b1n8GCP/TnFU4XgBVeiSkA9+4Rg6AzMzejdY1+JvWvfqCnRVM/ + GkOnMzZb17tyZi+ck8OKC/IcHkAyUYFWL0GWQSOojvBsPQxt+0V8aEIwsHjNSSha + nyNpAoGAd0XqdByRxbWgg5ZsvM0tvrpMITpEZsGMG9VeQPGl0wsQvC2zw5QGLvz/ + 57YhofOOr0M3yElcFA9Imvek5CYZsyL8eIWGZyadfRiYvGOUyvDDO3BYRG4DmhyF + KVk3URjEuOCC29ORvZ/7HaCO9iuEbvAA/mrAtd7KdCA+3PzfEOw= + -----END RSA PRIVATE KEY-----"""; + PrivateKeyUtils.writePrivateKey("127.0.0.1", key); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/util/TimeExpressionUtilTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/util/TimeExpressionUtilTest.java new file mode 100644 index 0000000..407de1d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/util/TimeExpressionUtilTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Test; + +/** + * test for TimeExpressionUtil + */ +class TimeExpressionUtilTest { + + + LocalDateTime now = LocalDateTime.of(2022, 4, 24, 2, 40, 0); + + @Test + void testBlankTemplate() { + assertEquals("", TimeExpressionUtil.calculate("", LocalDateTime.now())); + assertNull(TimeExpressionUtil.calculate(null, LocalDateTime.now())); + } + + @Test + void testErrorTemplate() { + assertEquals("hello", TimeExpressionUtil.calculate("hello", LocalDateTime.now())); + assertEquals("${}", TimeExpressionUtil.calculate("${}", LocalDateTime.now())); + assertEquals("${@current-24m}", TimeExpressionUtil.calculate("${@current-24m}", LocalDateTime.now())); + } + + @Test + void testWithSpace() { + assertEquals("2022-04-24 02:40:01", TimeExpressionUtil.calculate("${@now + 1s}", now)); + } + + @Test + void testCalculate() { + assertEquals("2022-04-24 02:40:00", TimeExpressionUtil.calculate("${@now}", now)); + assertEquals("2022-04-24", TimeExpressionUtil.calculate("${@date}", now)); + assertEquals("02:40:00", TimeExpressionUtil.calculate("${@time}", now)); + assertEquals("1650768000000", TimeExpressionUtil.calculate("${@timestamp}", now)); + assertEquals("1650768000", TimeExpressionUtil.calculate("${@timestamp10}", now)); + assertEquals("2022", TimeExpressionUtil.calculate("${@year}", now)); + assertEquals("04", TimeExpressionUtil.calculate("${@month}", now)); + assertEquals("24", TimeExpressionUtil.calculate("${@day}", now)); + assertEquals("02", TimeExpressionUtil.calculate("${@hour}", now)); + assertEquals("40", TimeExpressionUtil.calculate("${@minute}", now)); + assertEquals("00", TimeExpressionUtil.calculate("${@second}", now)); + assertEquals("000", TimeExpressionUtil.calculate("${@millisecond}", now)); + + assertEquals("2022-04-24 02:25:00", TimeExpressionUtil.calculate("${@now-15m}", now)); + assertEquals("2022-05-01 02:40:00", TimeExpressionUtil.calculate("${@now+1w}", now)); + assertEquals("2022-05-01 02:25:00", TimeExpressionUtil.calculate("${@now+1w-15m}", now)); + assertEquals("2022-04-25 02:40:00", TimeExpressionUtil.calculate("${@now+1d}", now)); + assertEquals("02:40:00", TimeExpressionUtil.calculate("${@time+1d}", now)); + assertEquals("2023-04-24 02:40:00", TimeExpressionUtil.calculate("${@now+1y}", now)); + } + + @Test + void testComplexTemplate() { + assertEquals("Three days after 2022-04-24 , is 2022-04-27", TimeExpressionUtil.calculate("Three days after ${@date} , is ${@date+3d}", now)); + assertEquals("2022年04月24日 02:40:00", TimeExpressionUtil.calculate("${@year}年${@month}月${@day}日 ${@time}", now)); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/pom.xml b/hertzbeat-collector/hertzbeat-collector-collector/pom.xml new file mode 100644 index 0000000..2e0a08d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/pom.xml @@ -0,0 +1,571 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-collector-collector</artifactId> + <name>${project.artifactId}</name> + + <properties> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-common-spring</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + + <!-- collector-basic --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-basic</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-mysql-r2dbc</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + + <!-- collector-kafka --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-kafka</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + + <!-- collector-mongodb --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-mongodb</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + + <!-- collector-nebulagraph --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-nebulagraph</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + + <!-- collector-rocketmq --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-rocketmq</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + + <!-- spring --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-configuration-processor</artifactId> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-autoconfigure</artifactId> + </dependency> + <!-- metrics --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.mysql</groupId> + <artifactId>mysql-connector-j</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers</artifactId> + <version>${testcontainers.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.microsoft.sqlserver</groupId> + <artifactId>mssql-jdbc</artifactId> + <scope>test</scope> + </dependency> + + </dependencies> + + <build> + <finalName>apache-hertzbeat-collector-${hzb.version}</finalName> + </build> + + <profiles> + <profile> + <id>inner</id> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>${maven-jar-plugin.version}</version> + <executions> + <execution> + <phase>package</phase> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>cluster</id> + <build> + <resources> + <resource> + <directory>src/main/resources</directory> + <filtering>true</filtering> + <includes> + <include>*.yml</include> + <include>*.properties</include> + <include>*.xml</include> + <include>banner.txt</include> + <include>META-INF/**</include> + </includes> + </resource> + </resources> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>${java.version}</source> + <target>${java.version}</target> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>${maven-jar-plugin.version}</version> + <configuration> + <classesDirectory>target/classes/</classesDirectory> + <archive> + <!--Generated JAR does not include Maven descriptor-related files--> + <addMavenDescriptor>false</addMavenDescriptor> + <manifest> + <!--Project startup class--> + <mainClass>org.apache.hertzbeat.collector.Collector</mainClass> + <useUniqueVersions>false</useUniqueVersions> + <!--Third-party JARs are added to the classpath using maven-dependency-plugin--> + <addClasspath>true</addClasspath> + <!--Location of external dependency JARs--> + <classpathPrefix>lib/</classpathPrefix> + </manifest> + <manifestEntries> + <Class-Path>. config</Class-Path> + </manifestEntries> + </archive> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <version>${maven-assembly-plugin.version}</version> + <executions> + <execution> + <id>without-jdk</id> + <!--Bound Maven operation--> + <phase>package</phase> + <!--Run once--> + <goals> + <goal>single</goal> + </goals> + <configuration> + <descriptors> + <descriptor>../../script/assembly/collector/assembly.xml</descriptor> + </descriptors> + <outputDirectory>../../dist</outputDirectory> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>runtime</id> + <build> + <resources> + <resource> + <directory>src/main/resources</directory> + <filtering>true</filtering> + <includes> + <include>*.yml</include> + <include>*.properties</include> + <include>*.xml</include> + <include>banner.txt</include> + <include>META-INF/**</include> + </includes> + </resource> + </resources> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>${java.version}</source> + <target>${java.version}</target> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>${maven-jar-plugin.version}</version> + <configuration> + <classesDirectory>target/classes/</classesDirectory> + <archive> + <!--Generated JAR does not include Maven descriptor-related files--> + <addMavenDescriptor>false</addMavenDescriptor> + <manifest> + <!--Project startup class--> + <mainClass>org.apache.hertzbeat.collector.Collector</mainClass> + <useUniqueVersions>false</useUniqueVersions> + <!--Third-party JARs are added to the classpath using maven-dependency-plugin--> + <addClasspath>true</addClasspath> + <!--Location of external dependency JARs--> + <classpathPrefix>lib/</classpathPrefix> + </manifest> + <manifestEntries> + <Class-Path>. config</Class-Path> + </manifestEntries> + </archive> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <version>${maven-assembly-plugin.version}</version> + <executions> + <execution> + <id>without-jdk</id> + <!--Bound Maven operation--> + <phase>package</phase> + <!--Run once--> + <goals> + <goal>single</goal> + </goals> + <configuration> + <descriptors> + <descriptor>../../script/assembly/collector/assembly.xml</descriptor> + </descriptors> + <outputDirectory>../../dist</outputDirectory> + </configuration> + </execution> + <execution> + <id>make-macos-arm64</id> + <!--Bound Maven operation--> + <phase>package</phase> + <!--Run once--> + <goals> + <goal>single</goal> + </goals> + <configuration> + <descriptors> + <descriptor>../../script/assembly/collector/assembly-macos-arm64.xml</descriptor> + </descriptors> + <outputDirectory>../../dist</outputDirectory> + </configuration> + </execution> + <execution> + <id>make-macos-amd64</id> + <!--Bound Maven operation--> + <phase>package</phase> + <!--Run once--> + <goals> + <goal>single</goal> + </goals> + <configuration> + <descriptors> + <descriptor>../../script/assembly/collector/assembly-macos-amd64.xml</descriptor> + </descriptors> + <outputDirectory>../../dist</outputDirectory> + </configuration> + </execution> + <execution> + <id>make-linux-arm64</id> + <!--Bound Maven operation--> + <phase>package</phase> + <!--Run once--> + <goals> + <goal>single</goal> + </goals> + <configuration> + <descriptors> + <descriptor>../../script/assembly/collector/assembly-linux-arm64.xml</descriptor> + </descriptors> + <outputDirectory>../../dist</outputDirectory> + </configuration> + </execution> + <execution> + <id>make-linux-amd64</id> + <!--Bound Maven operation--> + <phase>package</phase> + <!--Run once--> + <goals> + <goal>single</goal> + </goals> + <configuration> + <descriptors> + <descriptor>../../script/assembly/collector/assembly-linux-amd64.xml</descriptor> + </descriptors> + <outputDirectory>../../dist</outputDirectory> + </configuration> + </execution> + <execution> + <id>make-windows-64</id> + <!--Bound Maven operation--> + <phase>package</phase> + <!--Run once--> + <goals> + <goal>single</goal> + </goals> + <configuration> + <descriptors> + <descriptor>../../script/assembly/collector/assembly-windows-64.xml</descriptor> + </descriptors> + <outputDirectory>../../dist</outputDirectory> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>native</id> + <properties> + <native.image.name>hertzbeat-collector-collector</native.image.name> + <native.binary.extension></native.binary.extension> + <native.launcher.dir>../../script/assembly/collector/bin-native</native.launcher.dir> + <native.package.format>tar.gz</native.package.format> + <native.target.platform>unsupported</native.target.platform> + <native.package.id>${native.target.platform}-bin</native.package.id> + <native.package.baseDirectory>apache-hertzbeat-collector-native-${hzb.version}-${native.target.platform}-bin</native.package.baseDirectory> + <native.binary.source>target/${native.image.name}${native.binary.extension}</native.binary.source> + <native.executable.packageName>${project.build.finalName}${native.binary.extension}</native.executable.packageName> + </properties> + <build> + <finalName>apache-hertzbeat-collector-native-${hzb.version}</finalName> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <proc>full</proc> + </configuration> + </plugin> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-enforcer-plugin</artifactId> + <version>3.5.0</version> + <executions> + <execution> + <id>require-supported-native-platform</id> + <phase>validate</phase> + <goals> + <goal>enforce</goal> + </goals> + <configuration> + <rules> + <requireProperty> + <property>native.target.platform</property> + <regex>^(linux-amd64|linux-arm64|macos-amd64|macos-arm64|windows-amd64)$</regex> + <message>Unsupported native collector platform. Build the native collector on a supported GitHub runner or matching local host: linux-amd64, linux-arm64, macos-amd64, macos-arm64, windows-amd64.</message> + </requireProperty> + </rules> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.graalvm.buildtools</groupId> + <artifactId>native-maven-plugin</artifactId> + <version>0.11.4</version> + <executions> + <execution> + <id>build-native-image</id> + <phase>package</phase> + <goals> + <goal>compile-no-fork</goal> + </goals> + </execution> + </executions> + <configuration> + <imageName>${native.image.name}</imageName> + <buildArgs> + <buildArg>-H:+UnlockExperimentalVMOptions</buildArg> + <buildArg>-H:-AddAllFileSystemProviders</buildArg> + <buildArg>-H:ServiceLoaderFeatureExcludeServiceProviders=org.apache.sshd.common.file.root.RootedFileSystemProvider,org.apache.sshd.sftp.client.fs.SftpFileSystemProvider</buildArg> + <buildArg>--initialize-at-build-time=net.i2p.crypto.eddsa.EdDSASecurityProvider,org.apache.arrow.memory.util.MemoryUtil</buildArg> + <buildArg>-J--add-opens=java.base/java.nio=ALL-UNNAMED</buildArg> + <buildArg>-J-Dorg.apache.sshd.security.registrars=org.apache.sshd.common.util.security.eddsa.EdDSASecurityProviderRegistrar</buildArg> + </buildArgs> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <version>${maven-assembly-plugin.version}</version> + <executions> + <execution> + <id>native-bin</id> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + <configuration> + <descriptors> + <descriptor>../../script/assembly/collector/assembly-native.xml</descriptor> + </descriptors> + <outputDirectory>../../dist</outputDirectory> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>native-platform-linux-amd64</id> + <activation> + <os> + <name>Linux</name> + <arch>amd64</arch> + </os> + </activation> + <properties> + <native.target.platform>linux-amd64</native.target.platform> + </properties> + </profile> + <profile> + <id>native-platform-linux-x86_64</id> + <activation> + <os> + <name>Linux</name> + <arch>x86_64</arch> + </os> + </activation> + <properties> + <native.target.platform>linux-amd64</native.target.platform> + </properties> + </profile> + <profile> + <id>native-platform-linux-arm64</id> + <activation> + <os> + <name>Linux</name> + <arch>aarch64</arch> + </os> + </activation> + <properties> + <native.target.platform>linux-arm64</native.target.platform> + </properties> + </profile> + <profile> + <id>native-platform-macos-amd64</id> + <activation> + <os> + <family>mac</family> + <arch>x86_64</arch> + </os> + </activation> + <properties> + <native.target.platform>macos-amd64</native.target.platform> + </properties> + </profile> + <profile> + <id>native-platform-macos-arm64</id> + <activation> + <os> + <family>mac</family> + <arch>aarch64</arch> + </os> + </activation> + <properties> + <native.target.platform>macos-arm64</native.target.platform> + </properties> + </profile> + <profile> + <id>native-platform-windows-amd64</id> + <activation> + <os> + <family>windows</family> + <arch>amd64</arch> + </os> + </activation> + <properties> + <native.binary.extension>.exe</native.binary.extension> + <native.launcher.dir>../../script/assembly/collector/bin-native-win</native.launcher.dir> + <native.package.format>zip</native.package.format> + <native.target.platform>windows-amd64</native.target.platform> + </properties> + </profile> + <profile> + <id>native-platform-windows-x86_64</id> + <activation> + <os> + <family>windows</family> + <arch>x86_64</arch> + </os> + </activation> + <properties> + <native.binary.extension>.exe</native.binary.extension> + <native.launcher.dir>../../script/assembly/collector/bin-native-win</native.launcher.dir> + <native.package.format>zip</native.package.format> + <native.target.platform>windows-amd64</native.target.platform> + </properties> + </profile> + </profiles> +</project> diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/Collector.java b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/Collector.java new file mode 100644 index 0000000..0eca3ce --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/Collector.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector; + +import jakarta.annotation.PostConstruct; +import org.apache.hertzbeat.collector.nativex.CollectorRuntimeHintsRegistrar; +import org.apache.hertzbeat.collector.nativex.NativeCollectorDefaults; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.ImportRuntimeHints; + +/** + * collector startup + */ +@ComponentScan(basePackages = {"org.apache.hertzbeat"}) +@ConfigurationPropertiesScan(basePackages = {"org.apache.hertzbeat"}) +@SpringBootApplication +@ImportRuntimeHints(CollectorRuntimeHintsRegistrar.class) +public class Collector { + public static void main(String[] args) { + SpringApplication application = new SpringApplication(Collector.class); + NativeCollectorDefaults.applyTo(application); + application.run(args); + } + + @PostConstruct + public void init() { + System.setProperty("jdk.jndi.object.factoriesFilter", "!com.zaxxer.hikari.HikariJNDIFactory"); + if (System.getProperty("arrow.allocation.manager.type") == null + && System.getenv("ARROW_ALLOCATION_MANAGER_TYPE") == null) { + System.setProperty("arrow.allocation.manager.type", "Netty"); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlCollectorProperties.java b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlCollectorProperties.java new file mode 100644 index 0000000..ac34a37 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlCollectorProperties.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.database.mysql; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Collector-side MySQL query engine routing. + */ +@Getter +@Setter +@ConfigurationProperties(prefix = "hertzbeat.collector.mysql") +public class MysqlCollectorProperties { + + private QueryEngine queryEngine = QueryEngine.AUTO; + + public QueryEngine resolveQueryEngine(boolean mysqlJdbcDriverAvailable) { + if (queryEngine == QueryEngine.AUTO) { + return mysqlJdbcDriverAvailable ? QueryEngine.JDBC : QueryEngine.R2DBC; + } + return queryEngine; + } + + public boolean useR2dbc(boolean mysqlJdbcDriverAvailable) { + return resolveQueryEngine(mysqlJdbcDriverAvailable) == QueryEngine.R2DBC; + } + + /** + * Supported collector-side query engines. + */ + public enum QueryEngine { + AUTO, + JDBC, + R2DBC + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcDriverAvailability.java b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcDriverAvailability.java new file mode 100644 index 0000000..8c1466a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcDriverAvailability.java @@ -0,0 +1,80 @@ +/* + * 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.collector.collect.database.mysql; + +import java.net.URL; +import java.security.CodeSource; +import java.util.Locale; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Detects whether a MySQL JDBC driver is available from the external ext-lib path. + */ +@Component +public class MysqlJdbcDriverAvailability { + + private static final String[] MYSQL_DRIVER_CLASSES = { + "com.mysql.cj.jdbc.Driver", + "com.mysql.jdbc.Driver" + }; + + public boolean hasMysqlJdbcDriver() { + ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + for (String driverClass : MYSQL_DRIVER_CLASSES) { + if (!ClassUtils.isPresent(driverClass, classLoader)) { + continue; + } + try { + if (isExternalExtLibDriver(ClassUtils.forName(driverClass, classLoader))) { + return true; + } + } catch (ClassNotFoundException ignored) { + // Race-free enough for runtime detection: keep probing other known driver class names. + } + } + return false; + } + + boolean isExternalExtLibDriver(Class<?> driverClass) { + String location = resolveLocation(driverClass); + return isExtLibLocation(location); + } + + static boolean isExtLibLocation(String location) { + if (!StringUtils.hasText(location)) { + return false; + } + String normalized = location + .replace('\\', '/') + .toLowerCase(Locale.ROOT); + return normalized.contains("/ext-lib/"); + } + + private String resolveLocation(Class<?> driverClass) { + CodeSource codeSource = driverClass.getProtectionDomain().getCodeSource(); + if (codeSource != null && codeSource.getLocation() != null) { + return codeSource.getLocation().toExternalForm(); + } + String resourceName = ClassUtils.convertClassNameToResourcePath(driverClass.getName()) + ".class"; + ClassLoader classLoader = driverClass.getClassLoader(); + URL resource = classLoader != null ? classLoader.getResource(resourceName) : ClassLoader.getSystemResource(resourceName); + return resource != null ? resource.toExternalForm() : null; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlR2dbcJdbcQueryExecutor.java b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlR2dbcJdbcQueryExecutor.java new file mode 100644 index 0000000..5583709 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlR2dbcJdbcQueryExecutor.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.database.mysql; + +import java.net.URI; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.apache.hertzbeat.collector.collect.common.ssh.SshTunnelHelper; +import org.apache.hertzbeat.collector.collect.database.query.JdbcQueryExecutor; +import org.apache.hertzbeat.collector.collect.database.query.JdbcQueryExecutorRegistry; +import org.apache.hertzbeat.collector.collect.database.query.JdbcQueryRowSet; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlQueryExecutor; +import org.apache.hertzbeat.collector.mysql.r2dbc.QueryOptions; +import org.apache.hertzbeat.collector.mysql.r2dbc.QueryResult; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.SshTunnel; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +/** + * MySQL-compatible query-only adapter that lets JdbcCommonCollect execute read-only queries through the built-in + * R2DBC path when no MySQL JDBC driver is present. + */ +@Component +public class MysqlR2dbcJdbcQueryExecutor implements JdbcQueryExecutor, InitializingBean, DisposableBean { + + private static final String QUERY_TYPE_ONE_ROW = "oneRow"; + private static final String QUERY_TYPE_MULTI_ROW = "multiRow"; + private static final String QUERY_TYPE_COLUMNS = "columns"; + + private final MysqlCollectorProperties properties; + private final MysqlQueryExecutor mysqlQueryExecutor; + private final MysqlJdbcDriverAvailability mysqlJdbcDriverAvailability; + + public MysqlR2dbcJdbcQueryExecutor(MysqlCollectorProperties properties, + MysqlQueryExecutor mysqlQueryExecutor, + MysqlJdbcDriverAvailability mysqlJdbcDriverAvailability) { + this.properties = properties; + this.mysqlQueryExecutor = mysqlQueryExecutor; + this.mysqlJdbcDriverAvailability = mysqlJdbcDriverAvailability; + } + + @Override + public boolean supports(Metrics metrics) { + if (metrics == null || metrics.getJdbc() == null) { + return false; + } + JdbcProtocol jdbcProtocol = metrics.getJdbc(); + if (!isMysqlCompatiblePlatform(jdbcProtocol.getPlatform())) { + return false; + } + String queryType = jdbcProtocol.getQueryType(); + return properties.useR2dbc(mysqlJdbcDriverAvailability.hasMysqlJdbcDriver()) + && (QUERY_TYPE_ONE_ROW.equals(queryType) + || QUERY_TYPE_MULTI_ROW.equals(queryType) + || QUERY_TYPE_COLUMNS.equals(queryType)); + } + + private boolean isMysqlCompatiblePlatform(String platform) { + return "mysql".equalsIgnoreCase(platform) || "mariadb".equalsIgnoreCase(platform); + } + + @Override + public JdbcQueryRowSet executeQuery(Metrics metrics, int timeout, int maxRows) { + JdbcProtocol jdbcProtocol = metrics.getJdbc(); + QueryOptions options = buildQueryOptions(jdbcProtocol, timeout, maxRows); + QueryResult queryResult = mysqlQueryExecutor.execute(jdbcProtocol.getSql(), options); + if (queryResult.hasError()) { + throw new IllegalStateException("R2DBC MySQL query failed: " + queryResult.getError()); + } + return new QueryResultRowSet(queryResult); + } + + @Override + public void afterPropertiesSet() { + JdbcQueryExecutorRegistry.register(this); + } + + @Override + public void destroy() { + JdbcQueryExecutorRegistry.unregister(this); + } + + private QueryOptions buildQueryOptions(JdbcProtocol jdbcProtocol, int timeout, int maxRows) { + MysqlTarget target = resolveTarget(jdbcProtocol); + SshTunnel sshTunnel = jdbcProtocol.getSshTunnel(); + String host = target.host(); + int port = target.port(); + if (sshTunnel != null && Boolean.parseBoolean(sshTunnel.getEnable())) { + try { + int localPort = SshTunnelHelper.localPortForward(sshTunnel, host, String.valueOf(port)); + host = "127.0.0.1"; + port = localPort; + } catch (Exception exception) { + throw new IllegalStateException("R2DBC MySQL query adapter failed to establish SSH tunnel", exception); + } + } + return QueryOptions.builder() + .host(host) + .port(port) + .username(jdbcProtocol.getUsername()) + .password(jdbcProtocol.getPassword()) + .database(target.database()) + .schema(target.database()) + .timeout(Duration.ofMillis(timeout)) + .maxRows(maxRows) + .fetchSize(256) + .readOnly(true) + .build(); + } + + private MysqlTarget resolveTarget(JdbcProtocol jdbcProtocol) { + if (StringUtils.hasText(jdbcProtocol.getUrl())) { + return parseJdbcUrl(jdbcProtocol.getUrl(), jdbcProtocol.getDatabase()); + } + if (!StringUtils.hasText(jdbcProtocol.getHost()) || !StringUtils.hasText(jdbcProtocol.getPort())) { + throw new IllegalArgumentException("R2DBC MySQL query adapter requires host/port or a jdbc:mysql URL"); + } + return new MysqlTarget(jdbcProtocol.getHost(), Integer.parseInt(jdbcProtocol.getPort()), jdbcProtocol.getDatabase()); + } + + private MysqlTarget parseJdbcUrl(String url, String fallbackDatabase) { + String trimmed = url.trim(); + if (!(trimmed.startsWith("jdbc:mysql://") || trimmed.startsWith("jdbc:mariadb://"))) { + throw new IllegalArgumentException("R2DBC MySQL query adapter only supports jdbc:mysql:// or jdbc:mariadb:// URLs"); + } + URI uri = URI.create(trimmed.substring("jdbc:".length())); + String host = uri.getHost(); + int port = uri.getPort() > 0 ? uri.getPort() : 3306; + if (!StringUtils.hasText(host)) { + throw new IllegalArgumentException("R2DBC MySQL query adapter URL must include a host"); + } + String path = uri.getPath(); + String database = StringUtils.hasText(path) && path.length() > 1 ? path.substring(1) : fallbackDatabase; + return new MysqlTarget(host, port, database); + } + + private record MysqlTarget(String host, int port, String database) { + } + + private static final class QueryResultRowSet implements JdbcQueryRowSet { + + private final List<List<String>> rows; + private final Map<String, Integer> columnIndexMap; + private int currentIndex = -1; + + private QueryResultRowSet(QueryResult queryResult) { + this.rows = queryResult.getRows(); + this.columnIndexMap = buildColumnIndexMap(queryResult.getColumns()); + } + + @Override + public boolean next() { + currentIndex++; + return currentIndex < rows.size(); + } + + @Override + public String getString(String column) { + Integer index = columnIndexMap.get(column.toLowerCase(Locale.ROOT)); + if (index == null) { + throw new IllegalArgumentException("Column not found in R2DBC MySQL result: " + column); + } + return getString(index + 1); + } + + @Override + public String getString(int index) { + if (currentIndex < 0 || currentIndex >= rows.size()) { + throw new IllegalStateException("R2DBC MySQL result cursor is not positioned on a row"); + } + int zeroBased = index - 1; + List<String> row = rows.get(currentIndex); + if (zeroBased < 0 || zeroBased >= row.size()) { + throw new IllegalArgumentException("Column index out of bounds in R2DBC MySQL result: " + index); + } + return row.get(zeroBased); + } + + @Override + public void close() { + // QueryResult is fully materialized, so there is nothing left to close here. + } + + private static Map<String, Integer> buildColumnIndexMap(List<String> columns) { + Map<String, Integer> indexMap = new HashMap<>(columns.size()); + for (int index = 0; index < columns.size(); index++) { + indexMap.put(columns.get(index).toLowerCase(Locale.ROOT), index); + } + return indexMap; + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/collect/strategy/CollectStrategyFactory.java b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/collect/strategy/CollectStrategyFactory.java new file mode 100644 index 0000000..28b9d2d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/collect/strategy/CollectStrategyFactory.java @@ -0,0 +1,68 @@ +/* + * 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.collector.collect.strategy; + +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +/** + * Specific metrics collection factory + */ +@Slf4j +@Configuration +@Order(value = Ordered.HIGHEST_PRECEDENCE + 1) +public class CollectStrategyFactory implements CommandLineRunner { + + /** + * strategy container + */ + private static final ConcurrentHashMap<String, AbstractCollect> COLLECT_STRATEGY = new ConcurrentHashMap<>(); + + /** + * get instance of this protocol collection + * @param protocol collect protocol + * @return implement of Metrics Collection + */ + public static AbstractCollect invoke(String protocol) { + return COLLECT_STRATEGY.get(protocol); + } + + @Override + public void run(String... args) throws Exception { + COLLECT_STRATEGY.clear(); + // spi load and registry protocol and collect instance + ServiceLoader<AbstractCollect> loader = ServiceLoader.load(AbstractCollect.class, AbstractCollect.class.getClassLoader()); + for (AbstractCollect collect : loader) { + COLLECT_STRATEGY.put(collect.supportProtocol(), collect); + } + if (COLLECT_STRATEGY.isEmpty()) { + throw new IllegalStateException( + "No collect strategies were registered. " + + "Verify META-INF/services/org.apache.hertzbeat.collector.collect.AbstractCollect " + + "is present on the runtime classpath."); + } + log.info("Registered {} collect strategies: {}", COLLECT_STRATEGY.size(), COLLECT_STRATEGY.keySet()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/config/CollectorAutoConfiguration.java b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/config/CollectorAutoConfiguration.java new file mode 100644 index 0000000..0d5ccda --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/config/CollectorAutoConfiguration.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.config; + +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.ComponentScan; + +/** + * Collector Auto Configuration + * @version 2.1 + */ + +@AutoConfiguration +@ComponentScan(basePackages = ConfigConstants.PkgConstant.PKG + + SignConstants.DOT + + ConfigConstants.FunctionModuleConstants.COLLECTOR) +public class CollectorAutoConfiguration { +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/CommonDispatcher.java b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/CommonDispatcher.java new file mode 100644 index 0000000..f915f2e --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/CommonDispatcher.java @@ -0,0 +1,382 @@ +/* + * 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.collector.dispatch; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.gson.Gson; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService; +import org.apache.hertzbeat.collector.metrics.HertzBeatMetricsCollector; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.collector.timer.TimerDispatch; +import org.apache.hertzbeat.collector.timer.WheelTimerTask; +import org.apache.hertzbeat.collector.dispatch.unit.UnitConvert; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Collection task and response data scheduler + */ +@Component +@Slf4j +public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatch { + + /** + * Collection task timeout value + */ + private static final long DURATION_TIME = 240_000L; + /** + * Trigger sub task max num + */ + private static final int MAX_SUB_TASK_NUM = 50; + /** + * Collect Response env config length + */ + private static final int ENV_CONFIG_SIZE = 1; + private static final Gson GSON = new Gson(); + /** + * Priority queue of index collection tasks + */ + private final MetricsCollectorQueue jobRequestQueue; + /** + * Time round task scheduler + */ + private final TimerDispatch timerDispatch; + /** + * collection data exporter + */ + private final CommonDataQueue commonDataQueue; + /** + * Metrics task and start time mapping map + */ + private final Map<String, MetricsTime> metricsTimeoutMonitorMap; + + private final List<UnitConvert> unitConvertList; + + private final WorkerPool workerPool; + + private final String collectorIdentity; + + @Autowired + private HertzBeatMetricsCollector metricsCollector; + + public CommonDispatcher(MetricsCollectorQueue jobRequestQueue, + TimerDispatch timerDispatch, + CommonDataQueue commonDataQueue, + WorkerPool workerPool, + CollectJobService collectJobService, + List<UnitConvert> unitConvertList) { + this.commonDataQueue = commonDataQueue; + this.jobRequestQueue = jobRequestQueue; + this.timerDispatch = timerDispatch; + this.unitConvertList = unitConvertList; + this.workerPool = workerPool; + this.collectorIdentity = collectJobService.getCollectorIdentity(); + this.metricsTimeoutMonitorMap = new ConcurrentHashMap<>(16); + this.start(); + } + + public void start() { + try { + // Pull the collection task from the task queue and put it into the thread pool for execution + workerPool.executeLongRunning(() -> { + Thread.currentThread().setName("metrics-task-dispatcher"); + while (!Thread.currentThread().isInterrupted()) { + MetricsCollect metricsCollect = null; + try { + metricsCollect = jobRequestQueue.getJob(); + if (metricsCollect != null) { + workerPool.executeJob(metricsCollect); + } + } catch (RejectedExecutionException rejected) { + log.warn("[Dispatcher]-the worker pool is full, reject this metrics task,put in queue again."); + if (metricsCollect != null) { + metricsCollect.setRunPriority((byte) (metricsCollect.getRunPriority() + 1)); + jobRequestQueue.addJob(metricsCollect); + } + } catch (InterruptedException interruptedException) { + log.info("[Dispatcher]-metrics-task-dispatcher has been interrupt to close."); + Thread.currentThread().interrupt(); + } catch (Exception e) { + log.error("[Dispatcher]-{}.", e.getMessage(), e); + } + } + log.info("Thread Interrupted, Shutdown the [metrics-task-dispatcher]"); + }); + // monitoring metrics collection task execution timeout + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat("metrics-task-timeout-monitor-%d") + .setDaemon(true) + .build(); + ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(1, threadFactory); + scheduledExecutor.scheduleWithFixedDelay(this::monitorCollectTaskTimeout, 2, 20, TimeUnit.SECONDS); + } catch (Exception e) { + log.error("Common Dispatcher error: {}.", e.getMessage(), e); + } + } + + private void monitorCollectTaskTimeout() { + try { + // Detect whether the collection unit of each metrics has timed out for 4 minutes, + // and if it times out, it will be discarded and an exception will be returned. + long deadline = System.currentTimeMillis() - DURATION_TIME; + for (Map.Entry<String, MetricsTime> entry : metricsTimeoutMonitorMap.entrySet()) { + MetricsTime metricsTime = entry.getValue(); + if (metricsTime.getStartTime() < deadline) { + // Metrics collection timeout + MetricsTime removedMetricsTime = metricsTimeoutMonitorMap.remove(entry.getKey()); + if (removedMetricsTime == null) { + continue; + } + WheelTimerTask timerJob = (WheelTimerTask) metricsTime.getTimeout().task(); + Job job = timerJob.getJob(); + // timeout metrics + if (metricsCollector != null) { + long duration = System.currentTimeMillis() - removedMetricsTime.getStartTime(); + metricsCollector.recordCollectMetrics(job, duration, "timeout"); + } + + CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() + .setId(job.getMonitorId()) + .setTenantId(job.getTenantId()) + .setApp(job.getApp()) + .setMetrics(metricsTime.getMetrics().getName()) + .setPriority(metricsTime.getMetrics().getPriority()) + .setTime(System.currentTimeMillis()) + .setCode(CollectRep.Code.TIMEOUT).setMsg("collect timeout").build(); + log.error("[Collect Timeout]: \n{}", metricsData); + if (metricsData.getPriority() == 0) { + dispatchCollectData(metricsTime.timeout, metricsTime.getMetrics(), metricsData); + } + } + } + } catch (Exception e) { + log.error("[Task Timeout Monitor]-{}.", e.getMessage(), e); + } + } + + @Override + public void dispatchMetricsTask(Timeout timeout) { + // Divide the collection task of a single application into corresponding collection tasks of the metrics under it. + // Put each collect task into the thread pool for scheduling + WheelTimerTask timerTask = (WheelTimerTask) timeout.task(); + Job job = timerTask.getJob(); + job.constructPriorMetrics(); + Set<Metrics> metricsSet = job.getNextCollectMetrics(null, true); + metricsSet.forEach(metrics -> { + MetricsCollect metricsCollect = new MetricsCollect(metrics, timeout, this, + collectorIdentity, unitConvertList); + jobRequestQueue.addJob(metricsCollect); + if (metrics.getPrometheus() != null) { + metricsTimeoutMonitorMap.put(String.valueOf(job.getId()), + new MetricsTime(System.currentTimeMillis(), metrics, timeout)); + } else { + metricsTimeoutMonitorMap.put(job.getId() + "-" + metrics.getName(), + new MetricsTime(System.currentTimeMillis(), metrics, timeout)); + } + }); + } + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, CollectRep.MetricsData metricsData) { + WheelTimerTask timerJob = (WheelTimerTask) timeout.task(); + Job job = timerJob.getJob(); + String monitorKey; + if (metrics.isHasSubTask()) { + monitorKey = job.getId() + "-" + metrics.getName() + "-sub-" + metrics.getSubTaskId(); + } else { + monitorKey = job.getId() + "-" + metrics.getName(); + } + MetricsTime metricsTime = metricsTimeoutMonitorMap.remove(monitorKey); + + // job completed metrics + if (metricsTime != null && metricsCollector != null) { + long duration = System.currentTimeMillis() - metricsTime.getStartTime(); + String status = metricsData.getCode() == CollectRep.Code.SUCCESS ? "success" : "fail"; + metricsCollector.recordCollectMetrics(job, duration, status); + } + if (metrics.isHasSubTask()) { + boolean isLastTask = metrics.consumeSubTaskResponse(metricsData); + if (isLastTask) { + metricsData = metrics.getSubTaskDataRef().get().build(); + } else { + return; + } + } + Set<Metrics> metricsSet = job.getNextCollectMetrics(metrics, false); + if (job.isCyclic()) { + if (log.isDebugEnabled()) { + log.debug("Cyclic Job: {} - {} - {}", job.getMonitorId(), job.getApp(), metricsData.getMetrics()); + for (CollectRep.ValueRow valueRow : metricsData.getValues()) { + for (CollectRep.Field field : metricsData.getFields()) { + log.debug("Field-->{},Value-->{}", field.getName(), valueRow.getColumns(metricsData.getFields().indexOf(field))); + } + } + } + // If metricsSet is null, it means that the execution is completed or whether the priority of the collection metrics is 0, that is, the availability collection metrics. + // If the availability collection fails, the next metrics scheduling will be cancelled and the next round of scheduling will be entered directly. + boolean isAvailableCollectFailed = metricsSet != null && !metricsSet.isEmpty() + && metrics.getPriority() == (byte) 0 && metricsData.getCode() != CollectRep.Code.SUCCESS; + if (metricsSet == null || isAvailableCollectFailed || job.isSd()) { + // The collection and execution task of this job are completed. + // The periodic task pushes the task to the time wheel again. + // First, determine the execution time of the task and the task collection interval. + if (!timeout.isCancelled()) { + timerDispatch.cyclicJob(timerJob); + } + } else if (!metricsSet.isEmpty()) { + // The execution of the current level metrics is completed, and the execution of the next level metrics starts + // use pre collect metrics data to replace next metrics config params + List<Map<String, Configmap>> configmapList = CollectUtil.getConfigmapFromPreCollectData(metricsData); + if (configmapList.size() == ENV_CONFIG_SIZE) { + job.addEnvConfigmaps(configmapList.get(0)); + } + for (Metrics metricItem : metricsSet) { + Set<String> cryPlaceholderFields = CollectUtil.matchCryPlaceholderField(GSON.toJsonTree(metricItem)); + if (cryPlaceholderFields.isEmpty()) { + MetricsCollect metricsCollect = new MetricsCollect(metricItem, timeout, this, + collectorIdentity, unitConvertList); + jobRequestQueue.addJob(metricsCollect); + metricsTimeoutMonitorMap.put(job.getId() + "-" + metricItem.getName(), + new MetricsTime(System.currentTimeMillis(), metricItem, timeout)); + continue; + } + boolean isSubTask = configmapList.stream().anyMatch(map -> map.keySet().stream().anyMatch(cryPlaceholderFields::contains)); + int subTaskNum = isSubTask ? Math.min(configmapList.size(), MAX_SUB_TASK_NUM) : 1; + AtomicInteger subTaskNumAtomic = new AtomicInteger(subTaskNum); + AtomicReference<CollectRep.MetricsData.Builder> metricsDataReference = new AtomicReference<>(); + for (int index = 0; index < subTaskNum; index++) { + Map<String, Configmap> configmap = new HashMap<>(job.getEnvConfigmaps()); + if (isSubTask) { + Map<String, Configmap> preConfigMap = configmapList.get(index); + configmap.putAll(preConfigMap); + } + Metrics metric = CollectUtil.replaceCryPlaceholderToMetrics(metricItem, configmap); + metric.setSubTaskNum(subTaskNumAtomic); + metric.setSubTaskId(index); + metric.setSubTaskDataRef(metricsDataReference); + MetricsCollect metricsCollect = new MetricsCollect(metric, timeout, this, + collectorIdentity, unitConvertList); + jobRequestQueue.addJob(metricsCollect); + metricsTimeoutMonitorMap.put(job.getId() + "-" + metric.getName() + "-sub-" + index, + new MetricsTime(System.currentTimeMillis(), metric, timeout)); + } + + } + } else { + // The list of metrics at the current execution level has not been fully executed. + // It needs to wait for the execution of other metrics task of the same level to complete the execution and enter the next level for execution. + } + // If it is an asynchronous periodic cyclic task, directly response the collected data + if (job.isSd()) { + CollectRep.MetricsData sdMetricsData = CollectRep.MetricsData.newBuilder(metricsData).build(); + commonDataQueue.sendServiceDiscoveryData(sdMetricsData); + } + commonDataQueue.sendMetricsData(metricsData); + } else { + // If it is a temporary one-time task, you need to wait for the collected data of all metrics task to be packaged and returned. + // Insert the current metrics data into the job for unified assembly + job.addCollectMetricsData(metricsData); + if (log.isDebugEnabled()) { + log.debug("One-time Job: {}", metricsData.getMetrics()); + for (CollectRep.ValueRow valueRow : metricsData.getValues()) { + for (CollectRep.Field field : metricsData.getFields()) { + log.debug("Field-->{},Value-->{}", field.getName(), valueRow.getColumns(metricsData.getFields().indexOf(field))); + } + } + } + + if (job.isSd() || metricsSet == null) { + // The collection and execution of all metrics of this job are completed + // and the result listener is notified of the combination of all metrics data + timerDispatch.responseSyncJobData(job.getId(), job.getResponseDataTemp()); + } else if (!metricsSet.isEmpty()) { + // The execution of the current level metrics is completed, and the execution of the next level metrics starts + metricsSet.forEach(metricItem -> { + MetricsCollect metricsCollect = new MetricsCollect(metricItem, timeout, this, + collectorIdentity, unitConvertList); + jobRequestQueue.addJob(metricsCollect); + metricsTimeoutMonitorMap.put(job.getId() + "-" + metricItem.getName(), + new MetricsTime(System.currentTimeMillis(), metricItem, timeout)); + }); + } else { + // The list of metrics task at the current execution level has not been fully executed. + // It needs to wait for the execution of other metrics task of the same level to complete the execution and enter the next level for execution. + } + } + } + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, List<CollectRep.MetricsData> metricsDataList) { + WheelTimerTask timerJob = (WheelTimerTask) timeout.task(); + Job job = timerJob.getJob(); + MetricsTime metricsTime = metricsTimeoutMonitorMap.remove(String.valueOf(job.getId())); + if (metricsTime != null && metricsCollector != null) { + long duration = System.currentTimeMillis() - metricsTime.getStartTime(); + // For a list, we consider it a success if at least one item is successful. + boolean isSuccess = metricsDataList.stream().anyMatch(item -> item.getCode() == CollectRep.Code.SUCCESS); + metricsCollector.recordCollectMetrics(job, duration, isSuccess ? "success" : "fail"); + } + if (job.isCyclic()) { + // The collection and execution of all task of this job are completed. + // The periodic task pushes the task to the time wheel again. + // First, determine the execution time of the task and the task collection interval. + if (!timeout.isCancelled()) { + timerDispatch.cyclicJob(timerJob); + } + // it is an asynchronous periodic cyclic task, directly response the collected data + metricsDataList.forEach(commonDataQueue::sendMetricsData); + } else { + // The collection and execution of all metrics of this job are completed + // and the result listener is notified of the combination of all metrics data + timerDispatch.responseSyncJobData(job.getId(), metricsDataList); + } + } + + + /** + * Metrics times. + */ + @Data + @AllArgsConstructor + protected static class MetricsTime { + private long startTime; + private Metrics metrics; + private Timeout timeout; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsCollect.java b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsCollect.java new file mode 100644 index 0000000..94b2022 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsCollect.java @@ -0,0 +1,506 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.dispatch; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.jexl3.JexlExpression; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.prometheus.PrometheusAutoCollectImpl; +import org.apache.hertzbeat.collector.collect.strategy.CollectStrategyFactory; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.collector.timer.WheelTimerTask; +import org.apache.hertzbeat.collector.dispatch.unit.UnitConvert; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.JexlExpressionRunner; +import org.apache.hertzbeat.common.util.Pair; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * metrics collection + */ +@Slf4j +@Data +public class MetricsCollect implements Runnable, Comparable<MetricsCollect> { + /** + * Scheduling alarm threshold time 100ms + */ + private static final long WARN_DISPATCH_TIME = 100; + /** + * collector identity + */ + protected String collectorIdentity; + /** + * tenant id + */ + protected long tenantId; + /** + * task id + */ + protected long id; + /** + * app type name + */ + protected String app; + /** + * metrics configuration + */ + protected Metrics metrics; + /** + * metadata + */ + protected Map<String, String> metadata; + /** + * labels + */ + protected Map<String, String> labels; + /** + * annotations + */ + protected Map<String, String> annotations; + /** + * time wheel timeout + */ + protected Timeout timeout; + /** + * Task and Data Scheduling + */ + protected CollectDataDispatch collectDataDispatch; + /** + * task execution priority + */ + protected byte runPriority; + /** + * Periodic collection or one-time collection true-periodic false-one-time + */ + protected boolean isCyclic; + /** + * Time for creating collection task + */ + protected long newTime; + /** + * Start time of the collection task + */ + protected long startTime; + /** + * Whether it is a service discovery job, true is yes, false is no + */ + protected boolean isSd; + /** + * Whether to use the Prometheus proxy + */ + protected boolean prometheusProxyMode; + + protected List<UnitConvert> unitConvertList; + + public MetricsCollect(Metrics metrics, Timeout timeout, + CollectDataDispatch collectDataDispatch, + String collectorIdentity, + List<UnitConvert> unitConvertList) { + this.newTime = System.currentTimeMillis(); + this.timeout = timeout; + this.metrics = metrics; + this.collectorIdentity = collectorIdentity; + WheelTimerTask timerJob = (WheelTimerTask) timeout.task(); + Job job = timerJob.getJob(); + this.id = job.getMonitorId(); + this.tenantId = job.getTenantId(); + this.app = job.getApp(); + this.metadata = job.getMetadata(); + this.labels = job.getLabels(); + this.annotations = job.getAnnotations(); + this.collectDataDispatch = collectDataDispatch; + this.isCyclic = job.isCyclic(); + this.isSd = job.isSd(); + this.prometheusProxyMode = job.isPrometheusProxyMode(); + this.unitConvertList = unitConvertList; + // Temporary one-time tasks are executed with high priority + if (isCyclic) { + runPriority = (byte) -1; + } else { + runPriority = (byte) 1; + } + } + + @Override + public void run() { + this.startTime = System.currentTimeMillis(); + setNewThreadName(id, app, startTime, metrics); + CollectRep.MetricsData.Builder response = CollectRep.MetricsData.newBuilder(); + response.setApp(app).setId(id).setTenantId(tenantId) + .setLabels(labels).setAnnotations(annotations).addMetadataAll(metadata); + // for prometheus auto or proxy mode + if (DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(metrics.getProtocol())) { + List<CollectRep.MetricsData> metricsData; + + // TODO: Refactor Prometheus metrics collection logic. + // The current implementation for proxy mode and auto mode needs review and potential simplification. + // Consider a more unified approach or clarify the conditions for each mode. + /* + // TODO USE PROXY MODE + if (prometheusProxyMode) { + List<CollectRep.MetricsData> proxyData = PrometheusProxyCollectImpl.getInstance().collect(response, metrics); + List<CollectRep.MetricsData> autoData = PrometheusAutoCollectImpl.getInstance().collect(response, metrics); + metricsData = new LinkedList<>(); + if (proxyData != null) { + metricsData.addAll(proxyData); + } + if (autoData != null) { + metricsData.addAll(autoData); + } + } else { + metricsData = PrometheusAutoCollectImpl.getInstance().collect(response, metrics); + } + */ + metricsData = PrometheusAutoCollectImpl.getInstance().collect(response, metrics); + validateResponse(metricsData == null ? null : metricsData.stream().findFirst().orElse(null)); + collectDataDispatch.dispatchCollectData(timeout, metrics, metricsData); + return; + } + response.setMetrics(metrics.getName()); + // According to the metrics collection protocol, application type, etc., + // dispatch to the real application metrics collection implementation class + AbstractCollect abstractCollect = CollectStrategyFactory.invoke(metrics.getProtocol()); + if (abstractCollect == null) { + log.error("[Dispatcher] - not support this: app: {}, metrics: {}, protocol: {}.", + app, metrics.getName(), metrics.getProtocol()); + response.setCode(CollectRep.Code.FAIL); + response.setMsg("not support " + app + ", " + + metrics.getName() + ", " + metrics.getProtocol()); + } else { + try { + abstractCollect.preCheck(metrics); + abstractCollect.collect(response, metrics); + } catch (Exception e) { + String msg = e.getMessage(); + if (msg == null && e.getCause() != null) { + msg = e.getCause().getMessage(); + } + if (e instanceof IllegalArgumentException) { + log.error("[Metrics PreCheck]: {}.", msg, e); + } else { + log.error("[Metrics Collect]: {}.", msg, e); + } + response.setCode(CollectRep.Code.FAIL); + if (msg != null) { + response.setMsg(msg); + } + } + } + // Alias attribute expression replacement calculation + if (fastFailed()) { + return; + } + calculateFields(metrics, response); + CollectRep.MetricsData metricsData = validateResponse(response); + collectDataDispatch.dispatchCollectData(timeout, metrics, metricsData); + } + + /** + * Calculate the real metrics value according to the calculates and aliasFields configuration + * + * @param metrics Metrics configuration + * @param collectData Data collection + */ + public void calculateFields(Metrics metrics, CollectRep.MetricsData.Builder collectData) { + collectData.setPriority(metrics.getPriority()); + List<CollectRep.Field> fieldList = new LinkedList<>(); + for (Metrics.Field field : metrics.getFields()) { + CollectRep.Field.Builder fieldBuilder = CollectRep.Field.newBuilder(); + fieldBuilder.setName(field.getField()).setType(field.getType()).setLabel(field.isLabel()); + if (field.getUnit() != null) { + fieldBuilder.setUnit(field.getUnit()); + } + fieldList.add(fieldBuilder.build()); + } + collectData.addAllFields(fieldList); + List<CollectRep.ValueRow> aliasRowList = collectData.getValuesList(); + if (aliasRowList == null || aliasRowList.isEmpty()) { + return; + } + collectData.clearValues(); + // Preprocess calculates first + if (metrics.getCalculates() == null) { + metrics.setCalculates(Collections.emptyList()); + } + // eg: database_pages=Database pages unconventional mapping + Map<String, String> fieldAliasMap = new HashMap<>(8); + Map<String, JexlExpression> fieldExpressionMap = metrics.getCalculates() + .stream() + .map(cal -> transformCal(cal, fieldAliasMap)) + .filter(Objects::nonNull) + .collect(Collectors.toMap(arr -> (String) arr[0], arr -> (JexlExpression) arr[1], (oldValue, newValue) -> newValue)); + + if (metrics.getUnits() == null) { + metrics.setUnits(Collections.emptyList()); + } + Map<String, Pair<String, String>> fieldUnitMap = metrics.getUnits() + .stream() + .map(this::transformUnit) + .filter(Objects::nonNull) + .collect(Collectors.toMap(arr -> (String) arr[0], arr -> (Pair<String, String>) arr[1], (oldValue, newValue) -> newValue)); + + List<Metrics.Field> fields = metrics.getFields(); + List<String> aliasFields = Optional.ofNullable(metrics.getAliasFields()).orElseGet(Collections::emptyList); + Map<String, String> aliasFieldValueMap = new HashMap<>(8); + Map<String, Object> fieldValueMap = new HashMap<>(8); + Map<String, Object> stringTypefieldValueMap = new HashMap<>(8); + Map<String, String> aliasFieldUnitMap = new HashMap<>(8); + CollectRep.ValueRow.Builder realValueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (CollectRep.ValueRow aliasRow : aliasRowList) { + for (int aliasIndex = 0; aliasIndex < aliasFields.size(); aliasIndex++) { + String aliasFieldValue = aliasRow.getColumns(aliasIndex); + String aliasField = aliasFields.get(aliasIndex); + if (!CommonConstants.NULL_VALUE.equals(aliasFieldValue)) { + aliasFieldValueMap.put(aliasField, aliasFieldValue); + // whether the alias field is a number + CollectUtil.DoubleAndUnit doubleAndUnit = CollectUtil + .extractDoubleAndUnitFromStr(aliasFieldValue); + if (doubleAndUnit != null && doubleAndUnit.getValue() != null) { + fieldValueMap.put(aliasField, doubleAndUnit.getValue()); + if (doubleAndUnit.getUnit() != null) { + aliasFieldUnitMap.put(aliasField, doubleAndUnit.getUnit()); + } + } else { + fieldValueMap.put(aliasField, aliasFieldValue); + } + stringTypefieldValueMap.put(aliasField, aliasFieldValue); + } else { + fieldValueMap.put(aliasField, null); + stringTypefieldValueMap.put(aliasField, null); + } + } + + for (Metrics.Field field : fields) { + String realField = field.getField(); + JexlExpression expression = fieldExpressionMap.get(realField); + String value = null; + String aliasFieldUnit = null; + if (expression != null) { + try { + Map<String, Object> context; + if (CommonConstants.TYPE_STRING == field.getType()) { + context = stringTypefieldValueMap; + } else { + for (Map.Entry<String, String> unitEntry : aliasFieldUnitMap.entrySet()) { + if (expression.getSourceText().contains(unitEntry.getKey())) { + aliasFieldUnit = unitEntry.getValue(); + break; + } + } + context = fieldValueMap; + } + + // Also executed when valueList is empty, covering pure string assignment expressions + Object objValue = JexlExpressionRunner.evaluate(expression, context); + + if (objValue != null) { + value = String.valueOf(objValue); + } + } catch (Exception e) { + log.warn("[calculates execute warning, use original value.] {}", e.getMessage()); + value = Optional.ofNullable(fieldValueMap.get(expression.getSourceText())) + .map(String::valueOf) + .orElse(null); + } + } else { + // does not exist then map the alias value + String aliasField = fieldAliasMap.get(realField); + if (aliasField != null) { + value = aliasFieldValueMap.get(aliasField); + } else { + value = aliasFieldValueMap.get(realField); + } + + if (value != null) { + final byte fieldType = field.getType(); + if (fieldType == CommonConstants.TYPE_NUMBER) { + CollectUtil.DoubleAndUnit doubleAndUnit = CollectUtil + .extractDoubleAndUnitFromStr(value); + final Double tempValue = doubleAndUnit == null ? null : doubleAndUnit.getValue(); + value = tempValue == null ? null : String.valueOf(tempValue); + aliasFieldUnit = doubleAndUnit == null ? null : doubleAndUnit.getUnit(); + } else if (fieldType == CommonConstants.TYPE_TIME) { + final int tempValue; + value = (tempValue = CommonUtil.parseTimeStrToSecond(value)) == -1 ? null : String.valueOf(tempValue); + } + } + } + + Pair<String, String> unitPair = fieldUnitMap.get(realField); + if (aliasFieldUnit != null) { + if (unitPair != null) { + unitPair.setLeft(aliasFieldUnit); + } else if (field.getUnit() != null && !aliasFieldUnit.equalsIgnoreCase(field.getUnit())) { + unitPair = Pair.of(aliasFieldUnit, field.getUnit()); + } + } + if (value != null && unitPair != null) { + for (UnitConvert unitConvert : unitConvertList) { + if (unitConvert.checkUnit(unitPair.getLeft()) && unitConvert.checkUnit(unitPair.getRight())) { + value = unitConvert.convert(value, unitPair.getLeft(), unitPair.getRight()); + } + } + } + // Handle metrics values that may have units such as 34%, 34Mb, and limit values to 4 decimal places + if (CommonConstants.TYPE_NUMBER == field.getType()) { + value = CommonUtil.parseDoubleStr(value, field.getUnit()); + } + if (value == null) { + value = CommonConstants.NULL_VALUE; + } + realValueRowBuilder.addColumn(value); + } + aliasFieldValueMap.clear(); + fieldValueMap.clear(); + aliasFieldUnitMap.clear(); + stringTypefieldValueMap.clear(); + CollectRep.ValueRow realValueRow = realValueRowBuilder.build(); + realValueRowBuilder.clear(); + // apply filter calculation to the real value row + if (!CollectionUtils.isEmpty(metrics.getFilters())) { + Map<String, Object> contextMap = new HashMap<>(8); + for (int i = 0; i < fields.size(); i++) { + Metrics.Field field = fields.get(i); + String value = realValueRow.getColumns(i); + contextMap.put(field.getField(), value); + } + boolean isMatch = false; + for (String filterExpr : metrics.getFilters()) { + try { + JexlExpression expression = JexlExpressionRunner.compile(filterExpr); + if ((Boolean) JexlExpressionRunner.evaluate(expression, contextMap)) { + isMatch = true; + break; + } + } catch (Exception e) { + log.warn("[metrics data row filters execute warning] {}.", e.getMessage()); + } + } + if (!isMatch) { + // ignore this data row + continue; + } + } + collectData.addValueRow(realValueRow); + } + } + + /** + * @param cal cal + * @param fieldAliasMap field alias map + * @return expr + */ + private Object[] transformCal(String cal, Map<String, String> fieldAliasMap) { + int splitIndex = cal.indexOf("="); + if (splitIndex < 0) { + return null; + } + String field = cal.substring(0, splitIndex).trim(); + String expressionStr = cal.substring(splitIndex + 1).trim().replace("\\#", "#"); + JexlExpression expression; + try { + expression = JexlExpressionRunner.compile(expressionStr); + } catch (Exception e) { + fieldAliasMap.put(field, expressionStr); + return null; + } + return new Object[]{field, expression}; + } + + /** + * transform unit + * + * @param unit unit + * @return units + */ + private Object[] transformUnit(String unit) { + int equalIndex = unit.indexOf("="); + int arrowIndex = unit.indexOf("->"); + if (equalIndex < 0 || arrowIndex < 0) { + return null; + } + String field = unit.substring(0, equalIndex).trim(); + String originUnit = unit.substring(equalIndex + 1, arrowIndex).trim(); + String newUnit = unit.substring(arrowIndex + 2).trim(); + return new Object[]{field, Pair.of(originUnit, newUnit)}; + } + + private boolean fastFailed() { + return this.timeout == null || this.timeout.isCancelled(); + } + + private CollectRep.MetricsData validateResponse(CollectRep.MetricsData.Builder builder) { + long endTime = System.currentTimeMillis(); + builder.setTime(endTime); + long runningTime = endTime - startTime; + long allTime = endTime - newTime; + if (startTime - newTime >= WARN_DISPATCH_TIME) { + log.warn("[Collector Dispatch Warn, Dispatch Use {}ms.", startTime - newTime); + } + if (builder.getCode() != CollectRep.Code.SUCCESS) { + log.info("[Collect Failed, Run {}ms, All {}ms] Reason: {}", runningTime, allTime, builder.getMsg()); + } else { + log.info("[Collect Success, Run {}ms, All {}ms].", runningTime, allTime); + } + return builder.build(); + } + + private void validateResponse(CollectRep.MetricsData metricsData) { + if (metricsData == null) { + log.error("[Collect Failed] Response metrics data is null."); + return; + } + long endTime = System.currentTimeMillis(); + long runningTime = endTime - startTime; + long allTime = endTime - newTime; + if (startTime - newTime >= WARN_DISPATCH_TIME) { + log.warn("[Collector Dispatch Warn, Dispatch Use {}ms.", startTime - newTime); + } + if (metricsData.getCode() != CollectRep.Code.SUCCESS) { + log.info("[Collect Failed, Run {}ms, All {}ms] Reason: {}", runningTime, allTime, metricsData.getMsg()); + } else { + log.info("[Collect Success, Run {}ms, All {}ms].", runningTime, allTime); + } + } + + private void setNewThreadName(long monitorId, String app, long startTime, Metrics metrics) { + String builder = monitorId + "-" + app + "-" + metrics.getName() + + "-" + String.valueOf(startTime).substring(9); + Thread.currentThread().setName(builder); + } + + @Override + public int compareTo(MetricsCollect collect) { + return collect.runPriority - this.runPriority; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsCollectorQueue.java b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsCollectorQueue.java new file mode 100644 index 0000000..9f13c1c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsCollectorQueue.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.dispatch; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.concurrent.PriorityBlockingQueue; + +/** + * queue of jobs to run + */ +@Component +@Slf4j +public class MetricsCollectorQueue { + + private final PriorityBlockingQueue<MetricsCollect> jobQueue; + + public MetricsCollectorQueue() { + jobQueue = new PriorityBlockingQueue<>(); + } + + public void addJob(MetricsCollect job) { + jobQueue.offer(job); + } + + public MetricsCollect getJob() throws InterruptedException { + return jobQueue.take(); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/metrics/HertzBeatMetricsCollector.java b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/metrics/HertzBeatMetricsCollector.java new file mode 100644 index 0000000..ebbee45 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/metrics/HertzBeatMetricsCollector.java @@ -0,0 +1,94 @@ +/* + * 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.collector.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.job.Job; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.Duration; +import java.util.Map; + +/** + * Service for managing and recording Micrometer metrics. + * This service uses Micrometer which integrates natively with Spring Boot Actuator. + */ +@Service +@Slf4j +public class HertzBeatMetricsCollector { + + private final MeterRegistry meterRegistry; + + public HertzBeatMetricsCollector(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + log.info("MetricsService initialized with MeterRegistry: {}", meterRegistry.getClass().getSimpleName()); + } + + /** + * Records the metrics for a completed collection sub-task. + * + * @param job The parent job containing monitor info. + * @param durationMillis The duration of the collection task in milliseconds. + * @param status The final status of the collection ("success", "fail", "timeout"). + */ + public void recordCollectMetrics(Job job, long durationMillis, String status) { + if (job == null) { + return; + } + + Map<String, String> metadata = job.getMetadata(); + String monitorName = metadata != null ? metadata.get("instancename") : "unknown"; + String monitorTarget = metadata != null ? metadata.get("instancehost") : "unknown"; + // todo can not get the host from service discovery + if (!StringUtils.hasText(monitorTarget)) { + monitorTarget = "unknown"; + } + + // Record collection count + Counter.builder("hertzbeat.collect.total") + .description("The total number of collection tasks executed") + .tag("status", status) + .tag("monitor_type", job.getApp()) + .tag("monitor_id", String.valueOf(job.getMonitorId())) + .tag("monitor_name", monitorName) + .tag("monitor_target", monitorTarget) + .register(meterRegistry) + .increment(); + + // Record collection duration + Timer.builder("hertzbeat.collect.duration") + .description("The duration of collection task executions") + .tag("status", status) + .tag("monitor_type", job.getApp()) + .tag("monitor_id", String.valueOf(job.getMonitorId())) + .tag("monitor_name", monitorName) + .tag("monitor_target", monitorTarget) + .register(meterRegistry) + .record(Duration.ofMillis(durationMillis)); + + if (log.isDebugEnabled()) { + log.debug("Recorded metrics for monitor [{}] ({}): status={}, duration={}ms", + monitorName, job.getMonitorId(), status, durationMillis); + } + } +} \ No newline at end of file diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/nativex/CollectorRuntimeHintsRegistrar.java b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/nativex/CollectorRuntimeHintsRegistrar.java new file mode 100644 index 0000000..3464caa --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/nativex/CollectorRuntimeHintsRegistrar.java @@ -0,0 +1,111 @@ +/* + * 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.collector.nativex; + +import java.util.LinkedHashSet; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.arrow.memory.netty.NettyAllocationManager; +import org.apache.arrow.vector.types.DateUnit; +import org.apache.arrow.vector.types.FloatingPointPrecision; +import org.apache.arrow.vector.types.IntervalUnit; +import org.apache.arrow.vector.types.MetadataVersion; +import org.apache.arrow.vector.types.TimeUnit; +import org.apache.arrow.vector.types.UnionMode; +import org.apache.arrow.vector.types.pojo.ArrowType; +import org.apache.arrow.vector.types.pojo.DictionaryEncoding; +import org.apache.arrow.vector.types.pojo.Field; +import org.apache.arrow.vector.types.pojo.FieldType; +import org.apache.arrow.vector.types.pojo.Schema; +import org.apache.hertzbeat.common.entity.dto.ServerInfo; +import org.springframework.aot.hint.BindingReflectionHintsRegistrar; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.lang.NonNull; +import org.springframework.util.ClassUtils; + +/** + * Registers native binding hints for collector-side message payloads. + */ +@Slf4j +public class CollectorRuntimeHintsRegistrar implements RuntimeHintsRegistrar { + + private static final String JOB_PACKAGE = "org.apache.hertzbeat.common.entity.job"; + private static final String JOB_PROTOCOL_PACKAGE = "org.apache.hertzbeat.common.entity.job.protocol"; + + @Override + public void registerHints(@NonNull RuntimeHints hints, ClassLoader classLoader) { + BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar(); + registerType(bindingRegistrar, hints, ServerInfo.class); + scanBindingPackage(classLoader, bindingRegistrar, hints, JOB_PACKAGE); + scanBindingPackage(classLoader, bindingRegistrar, hints, JOB_PROTOCOL_PACKAGE); + hints.reflection().registerType(NettyAllocationManager.class, MemberCategory.DECLARED_FIELDS); + registerType(bindingRegistrar, hints, Schema.class); + registerType(bindingRegistrar, hints, Field.class); + registerType(bindingRegistrar, hints, FieldType.class); + registerType(bindingRegistrar, hints, DictionaryEncoding.class); + registerType(bindingRegistrar, hints, DateUnit.class); + registerType(bindingRegistrar, hints, FloatingPointPrecision.class); + registerType(bindingRegistrar, hints, IntervalUnit.class); + registerType(bindingRegistrar, hints, MetadataVersion.class); + registerType(bindingRegistrar, hints, TimeUnit.class); + registerType(bindingRegistrar, hints, UnionMode.class); + for (Class<?> nestedClass : ArrowType.class.getDeclaredClasses()) { + if (!nestedClass.isAnnotation() && !nestedClass.isInterface()) { + registerType(bindingRegistrar, hints, nestedClass); + } + } + } + + private void scanBindingPackage(ClassLoader classLoader, BindingReflectionHintsRegistrar bindingRegistrar, + RuntimeHints hints, String basePackage) { + for (Class<?> clazz : findBindingTypes(basePackage, classLoader)) { + registerType(bindingRegistrar, hints, clazz); + } + } + + private void registerType(BindingReflectionHintsRegistrar bindingRegistrar, RuntimeHints hints, Class<?> clazz) { + bindingRegistrar.registerReflectionHints(hints.reflection(), clazz); + } + + private Set<Class<?>> findBindingTypes(String basePackage, ClassLoader classLoader) { + Set<Class<?>> bindingTypes = new LinkedHashSet<>(); + ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); + TypeFilter includeAll = (metadataReader, metadataReaderFactory) -> true; + scanner.addIncludeFilter(includeAll); + for (BeanDefinition candidate : scanner.findCandidateComponents(basePackage)) { + String className = candidate.getBeanClassName(); + if (className == null) { + continue; + } + try { + Class<?> clazz = ClassUtils.forName(className, classLoader); + if (!clazz.isAnnotation() && !clazz.isInterface()) { + bindingTypes.add(clazz); + } + } catch (Throwable ex) { + log.debug("Skip native binding hint registration for {}", className, ex); + } + } + return bindingTypes; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/nativex/NativeCollectorDefaults.java b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/nativex/NativeCollectorDefaults.java new file mode 100644 index 0000000..1926c48 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/nativex/NativeCollectorDefaults.java @@ -0,0 +1,56 @@ +/* + * 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.collector.nativex; + +import java.util.Map; +import org.springframework.boot.SpringApplication; +import org.springframework.core.NativeDetector; + +/** + * Applies collector defaults without forking {@code application.yml}. + */ +public final class NativeCollectorDefaults { + + static final String AUTOCONFIGURE_EXCLUDE_PROPERTY = "spring.autoconfigure.exclude"; + static final String JVM_AUTOCONFIGURE_EXCLUDES = String.join(",", + "org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration", + "org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration", + "org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration", + "org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration"); + static final String NATIVE_AUTOCONFIGURE_EXCLUDES = String.join(",", + JVM_AUTOCONFIGURE_EXCLUDES, + "org.springframework.boot.data.jpa.autoconfigure.DataJpaRepositoriesAutoConfiguration", + "org.springframework.boot.jdbc.autoconfigure.DataSourceInitializationAutoConfiguration", + "org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration", + "org.springframework.boot.jdbc.autoconfigure.health.DataSourceHealthContributorAutoConfiguration", + "org.springframework.boot.jdbc.autoconfigure.metrics.DataSourcePoolMetricsAutoConfiguration", + "org.springframework.boot.tomcat.autoconfigure.metrics.TomcatMetricsAutoConfiguration"); + + private NativeCollectorDefaults() { + } + + public static void applyTo(SpringApplication application) { + application.setDefaultProperties(defaultProperties(NativeDetector.inNativeImage())); + } + + static Map<String, Object> defaultProperties(boolean nativeImage) { + return Map.of( + AUTOCONFIGURE_EXCLUDE_PROPERTY, + nativeImage ? NATIVE_AUTOCONFIGURE_EXCLUDES : JVM_AUTOCONFIGURE_EXCLUDES); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/services/org.apache.hertzbeat.collector.collect.AbstractCollect b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/services/org.apache.hertzbeat.collector.collect.AbstractCollect new file mode 100644 index 0000000..49dac37 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/services/org.apache.hertzbeat.collector.collect.AbstractCollect @@ -0,0 +1,38 @@ +org.apache.hertzbeat.collector.collect.http.HttpCollectImpl +org.apache.hertzbeat.collector.collect.http.SslCertificateCollectImpl +org.apache.hertzbeat.collector.collect.database.JdbcCommonCollect +org.apache.hertzbeat.collector.collect.icmp.IcmpCollectImpl +org.apache.hertzbeat.collector.collect.jmx.JmxCollectImpl +org.apache.hertzbeat.collector.collect.redis.RedisCommonCollectImpl +org.apache.hertzbeat.collector.collect.mongodb.MongodbSingleCollectImpl +org.apache.hertzbeat.collector.collect.rocketmq.RocketmqSingleCollectImpl +org.apache.hertzbeat.collector.collect.snmp.SnmpCollectImpl +org.apache.hertzbeat.collector.collect.ssh.SshCollectImpl +org.apache.hertzbeat.collector.collect.telnet.TelnetCollectImpl +org.apache.hertzbeat.collector.collect.smtp.SmtpCollectImpl +org.apache.hertzbeat.collector.collect.ntp.NtpCollectImpl +org.apache.hertzbeat.collector.collect.websocket.WebsocketCollectImpl +org.apache.hertzbeat.collector.collect.ftp.FtpCollectImpl +org.apache.hertzbeat.collector.collect.udp.UdpCollectImpl +org.apache.hertzbeat.collector.collect.push.PushCollectImpl +org.apache.hertzbeat.collector.collect.dns.DnsCollectImpl +org.apache.hertzbeat.collector.collect.nginx.NginxCollectImpl +org.apache.hertzbeat.collector.collect.memcached.MemcachedCollectImpl +org.apache.hertzbeat.collector.collect.nebulagraph.NebulaGraphCollectImpl +org.apache.hertzbeat.collector.collect.pop3.Pop3CollectImpl +org.apache.hertzbeat.collector.collect.registry.RegistryImpl +org.apache.hertzbeat.collector.collect.redfish.RedfishCollectImpl +org.apache.hertzbeat.collector.collect.nebulagraph.NgqlCollectImpl +org.apache.hertzbeat.collector.collect.imap.ImapCollectImpl +org.apache.hertzbeat.collector.collect.script.ScriptCollectImpl +org.apache.hertzbeat.collector.collect.mqtt.MqttCollectImpl +org.apache.hertzbeat.collector.collect.ipmi2.IpmiCollectImpl +org.apache.hertzbeat.collector.collect.kafka.KafkaCollectImpl +org.apache.hertzbeat.collector.collect.sd.HttpSdCollectImpl +org.apache.hertzbeat.collector.collect.sd.NacosSdCollectImpl +org.apache.hertzbeat.collector.collect.sd.DnsSdCollectImpl +org.apache.hertzbeat.collector.collect.sd.EurekaSdCollectImpl +org.apache.hertzbeat.collector.collect.sd.ConsulSdCollectImpl +org.apache.hertzbeat.collector.collect.modbus.ModbusCollectImpl +org.apache.hertzbeat.collector.collect.s7.S7CollectImpl +org.apache.hertzbeat.collector.collect.sd.ZookeeperSdCollectImpl diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/spring.factories b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..731da64 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/spring.factories @@ -0,0 +1,17 @@ +# 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. + +# Intentionally left blank. +# Native collector defaults are applied from Collector.main. diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..88a0589 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,16 @@ +# 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. + +org.apache.hertzbeat.collector.config.CollectorAutoConfiguration diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/application.yml b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/application.yml new file mode 100644 index 0000000..03702ef --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/application.yml @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +server: + port: 1159 + shutdown: graceful + tomcat: + threads: + min-spare: 1 +spring: + application: + name: ${HOSTNAME:@hertzbeat-collector@}${PID} + profiles: + active: cluster + lifecycle: + timeout-per-shutdown-phase: 10s + jackson: + default-property-inclusion: ALWAYS +management: + endpoints: + web: + exposure: + include: + - 'prometheus' + endpoint: + prometheus: + access: read_only + metrics: + tags: + application: ${spring.application.name} + environment: ${spring.profiles.active} + prometheus: + metrics: + export: + enabled: true +--- +spring: + config: + activate: + on-profile: cluster + +collector: + info: + version: ${COLLECTOR_VERSION:1.8.0} + ip: ${COLLECTOR_IP:} + dispatch: + entrance: + netty: + enabled: true + # Config the collector unique identity name. + identity: ${IDENTITY:} + # Config the running mode(public or private), public cluster or private cloud-edge. + mode: ${MODE:public} + manager-host: ${MANAGER_HOST:}${MANAGER_IP:} + manager-port: ${MANAGER_PORT:1158} + +push: + uri: "127.0.0.1:1157" + + +common: + queue: + type: netty + +hertzbeat: + collector: + mysql: + # MySQL-compatible query engine routing for MySQL, MariaDB, OceanBase, and TiDB SQL metrics. + # auto : prefer JDBC only when mysql-connector-j is available from ext-lib, otherwise use the built-in query engine + # jdbc : always use JDBC + # r2dbc : always use the built-in query engine + query-engine: ${HERTZBEAT_COLLECTOR_MYSQL_QUERY_ENGINE:auto} + # Optional virtual-thread overrides. Remove this whole block to use built-in defaults. + vthreads: + enabled: true + common: + mode: UNBOUNDED_VT + collector: + mode: LIMIT_AND_REJECT diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/banner.txt b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/banner.txt new file mode 100644 index 0000000..cf90a00 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/banner.txt @@ -0,0 +1,5 @@ + _ _ _ ____ _ + | | | | ___ _ __| |_ ___| __ ) ___ __ _| |_ + | |_| |/ _ \ '__| __|_ / _ \ / _ \/ _` | __| Profile: ${spring.profiles.active} + | _ | __/ | | |_ / /| |_) | __/ (_| | |_ Name: ${spring.application.name} Port: ${server.port} Pid: ${pid} + |_| |_|\___|_| \__/___|____/ \___|\__,_|\__| https://hertzbeat.apache.org/ diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/logback-spring.xml b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..7d694dc --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/logback-spring.xml @@ -0,0 +1,136 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<configuration scan="true"> + <springProperty scope="context" name="application_name" source="spring.application.name" defaultValue="collector"/> + <!-- Output logs to ConsoleAppender --> + <appender name="CONSOLE_RAW" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <!--<pattern>%d %p (%file:%line\)- %m%n</pattern>--> + <!-- Format output: %d for date, %thread for thread name, %-5level for level with 5 character width, %msg for log message, %n for new line --> + <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger - %msg%n</pattern> + <charset>UTF-8</charset> + </encoder> + </appender> + + <appender name="ConsoleAppender" class="ch.qos.logback.classic.AsyncAppender"> + <discardingThreshold>0</discardingThreshold> + <queueSize>512</queueSize> + <includeCallerData>true</includeCallerData> + <appender-ref ref="CONSOLE_RAW"/> + </appender> + + <appender name="SystemOutFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <!-- Rolling policy for loggers, by date and by size --> + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <!-- Archive log file path. %d{yyyy-MM-dd} specifies date format, %i specifies index --> + <fileNamePattern>logs/${application_name}-%d{yyyy-MM-dd}.%i.log.zip</fileNamePattern> + <!-- Log retention duration --> + <maxHistory>7</maxHistory> + <!-- Maximum size of log retention --> + <totalSizeCap>5GB</totalSizeCap> + <cleanHistoryOnStart>true</cleanHistoryOnStart> + <!-- Besides logging by day, log files cannot exceed 200M, if exceeded, log files will start from index 0 --> + <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> + <maxFileSize>50MB</maxFileSize> + </timeBasedFileNamingAndTriggeringPolicy> + </rollingPolicy> + <!-- Append logs --> + <append>true</append> + <!-- Log file format --> + <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> + <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger Line:%-3L - %msg%n</pattern> + <charset>utf-8</charset> + </encoder> + </appender> + + <appender name="ErrOutFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <fileNamePattern>logs/${application_name}-%d{yyyy-MM-dd}-error.%i.log.zip</fileNamePattern> + <!-- Log retention duration --> + <maxHistory>7</maxHistory> + <!-- Maximum size of log retention --> + <totalSizeCap>5GB</totalSizeCap> + <cleanHistoryOnStart>true</cleanHistoryOnStart> + <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> + <maxFileSize>50MB</maxFileSize> + </timeBasedFileNamingAndTriggeringPolicy> + </rollingPolicy> + <!-- Append logs --> + <append>true</append> + <!-- Log file format --> + <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> + <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger Line:%-3L - %msg%n</pattern> + <charset>utf-8</charset> + </encoder> + <!-- This log file records error and above levels --> + <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> + <level>ERROR</level> + </filter> + </appender> + + <!-- Settings for this logger: for example, all output logs under the org.springframework package must be at level info or above to be output! --> + <!-- This can avoid outputting many common debug information of the spring framework! --> + <logger name="org.springframework" level="info" /> + <logger name="org.json" level="error"/> + <logger name="io.netty" level="info"/> + <logger name="org.slf4j" level="info"/> + <logger name="ch.qos.logback" level="info"/> + <logger name="org.apache.kafka.clients" level="info"/> + <logger name="org.hibernate" level="info"/> + <logger name="org.apache.http" level="info"/> + <logger name="com.zaxxer" level="info"/> + <logger name="springfox" level="info"/> + <logger name="org.mongodb" level="warn"/> + <logger name="io.greptime" level="warn"/> + + <!-- Production environment configuration --> + <springProfile name="prod"> + <root level="INFO"> + <appender-ref ref="ConsoleAppender"/> + <appender-ref ref="SystemOutFileAppender"/> + <appender-ref ref="ErrOutFileAppender"/> + </root> + </springProfile> + + <springProfile name="cluster"> + <root level="INFO"> + <appender-ref ref="ConsoleAppender"/> + <appender-ref ref="SystemOutFileAppender"/> + <appender-ref ref="ErrOutFileAppender"/> + </root> + </springProfile> + + <!-- Development environment configuration --> + <springProfile name="dev"> + <root level="INFO"> + <appender-ref ref="ConsoleAppender"/> + <appender-ref ref="SystemOutFileAppender"/> + <appender-ref ref="ErrOutFileAppender"/> + </root> + </springProfile> + + <!-- Development environment configuration --> + <springProfile name="mysql"> + <root level="INFO"> + <appender-ref ref="ConsoleAppender"/> + <appender-ref ref="SystemOutFileAppender"/> + <appender-ref ref="ErrOutFileAppender"/> + </root> + </springProfile> + +</configuration> diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/native/resources/META-INF/native-image/org.apache.hertzbeat/hertzbeat-collector-collector/reflect-config.json b/hertzbeat-collector/hertzbeat-collector-collector/src/native/resources/META-INF/native-image/org.apache.hertzbeat/hertzbeat-collector-collector/reflect-config.json new file mode 100644 index 0000000..227a978 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/native/resources/META-INF/native-image/org.apache.hertzbeat/hertzbeat-collector-collector/reflect-config.json @@ -0,0 +1,54 @@ +[ + { + "name": "org.apache.hertzbeat.collector.Collector__ApplicationContextInitializer", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "org.apache.hertzbeat.collector.Collector__BeanFactoryRegistrations", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "org.apache.hertzbeat.common.entity.dto.ServerInfo", + "allDeclaredConstructors": true, + "allDeclaredFields": true, + "allDeclaredMethods": true + }, + { + "name": "io.netty.channel.kqueue.KQueueDatagramChannel", + "allPublicConstructors": true + }, + { + "name": "io.netty.channel.kqueue.KQueueSocketChannel", + "allPublicConstructors": true + }, + { + "name": "io.netty.channel.kqueue.KQueueEventLoopGroup", + "allPublicConstructors": true + }, + { + "name": "io.netty.channel.epoll.EpollDatagramChannel", + "allPublicConstructors": true + }, + { + "name": "io.netty.channel.epoll.EpollSocketChannel", + "allPublicConstructors": true + }, + { + "name": "io.netty.channel.epoll.EpollEventLoopGroup", + "allPublicConstructors": true + }, + { + "name": "io.netty.channel.socket.nio.NioDatagramChannel", + "allPublicConstructors": true + }, + { + "name": "io.netty.channel.socket.nio.NioSocketChannel", + "allPublicConstructors": true + }, + { + "name": "io.netty.channel.nio.NioEventLoopGroup", + "allPublicConstructors": true + } +] diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/native/resources/META-INF/services/org.apache.hertzbeat.collector.collect.AbstractCollect b/hertzbeat-collector/hertzbeat-collector-collector/src/native/resources/META-INF/services/org.apache.hertzbeat.collector.collect.AbstractCollect new file mode 100644 index 0000000..49dac37 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/native/resources/META-INF/services/org.apache.hertzbeat.collector.collect.AbstractCollect @@ -0,0 +1,38 @@ +org.apache.hertzbeat.collector.collect.http.HttpCollectImpl +org.apache.hertzbeat.collector.collect.http.SslCertificateCollectImpl +org.apache.hertzbeat.collector.collect.database.JdbcCommonCollect +org.apache.hertzbeat.collector.collect.icmp.IcmpCollectImpl +org.apache.hertzbeat.collector.collect.jmx.JmxCollectImpl +org.apache.hertzbeat.collector.collect.redis.RedisCommonCollectImpl +org.apache.hertzbeat.collector.collect.mongodb.MongodbSingleCollectImpl +org.apache.hertzbeat.collector.collect.rocketmq.RocketmqSingleCollectImpl +org.apache.hertzbeat.collector.collect.snmp.SnmpCollectImpl +org.apache.hertzbeat.collector.collect.ssh.SshCollectImpl +org.apache.hertzbeat.collector.collect.telnet.TelnetCollectImpl +org.apache.hertzbeat.collector.collect.smtp.SmtpCollectImpl +org.apache.hertzbeat.collector.collect.ntp.NtpCollectImpl +org.apache.hertzbeat.collector.collect.websocket.WebsocketCollectImpl +org.apache.hertzbeat.collector.collect.ftp.FtpCollectImpl +org.apache.hertzbeat.collector.collect.udp.UdpCollectImpl +org.apache.hertzbeat.collector.collect.push.PushCollectImpl +org.apache.hertzbeat.collector.collect.dns.DnsCollectImpl +org.apache.hertzbeat.collector.collect.nginx.NginxCollectImpl +org.apache.hertzbeat.collector.collect.memcached.MemcachedCollectImpl +org.apache.hertzbeat.collector.collect.nebulagraph.NebulaGraphCollectImpl +org.apache.hertzbeat.collector.collect.pop3.Pop3CollectImpl +org.apache.hertzbeat.collector.collect.registry.RegistryImpl +org.apache.hertzbeat.collector.collect.redfish.RedfishCollectImpl +org.apache.hertzbeat.collector.collect.nebulagraph.NgqlCollectImpl +org.apache.hertzbeat.collector.collect.imap.ImapCollectImpl +org.apache.hertzbeat.collector.collect.script.ScriptCollectImpl +org.apache.hertzbeat.collector.collect.mqtt.MqttCollectImpl +org.apache.hertzbeat.collector.collect.ipmi2.IpmiCollectImpl +org.apache.hertzbeat.collector.collect.kafka.KafkaCollectImpl +org.apache.hertzbeat.collector.collect.sd.HttpSdCollectImpl +org.apache.hertzbeat.collector.collect.sd.NacosSdCollectImpl +org.apache.hertzbeat.collector.collect.sd.DnsSdCollectImpl +org.apache.hertzbeat.collector.collect.sd.EurekaSdCollectImpl +org.apache.hertzbeat.collector.collect.sd.ConsulSdCollectImpl +org.apache.hertzbeat.collector.collect.modbus.ModbusCollectImpl +org.apache.hertzbeat.collector.collect.s7.S7CollectImpl +org.apache.hertzbeat.collector.collect.sd.ZookeeperSdCollectImpl diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MariadbJdbcQueryAdapterTemplateIntegrationTest.java b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MariadbJdbcQueryAdapterTemplateIntegrationTest.java new file mode 100644 index 0000000..1ac6549 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MariadbJdbcQueryAdapterTemplateIntegrationTest.java @@ -0,0 +1,320 @@ +/* + * 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.collector.collect.database.mysql; + +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.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.hertzbeat.collector.collect.strategy.CollectStrategyFactory; +import org.apache.hertzbeat.collector.dispatch.CollectDataDispatch; +import org.apache.hertzbeat.collector.dispatch.MetricsCollect; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcConnectionFactoryProvider; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcQueryExecutor; +import org.apache.hertzbeat.collector.mysql.r2dbc.ResultSetMapper; +import org.apache.hertzbeat.collector.mysql.r2dbc.SqlGuard; +import org.apache.hertzbeat.collector.timer.WheelTimerTask; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.yaml.snakeyaml.Yaml; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MariadbJdbcQueryAdapterTemplateIntegrationTest { + + private static final String TEST_DATABASE = "hzb"; + private static final String TEST_USERNAME = "test"; + private static final String TEST_PASSWORD = "test123"; + private static final String ROOT_PASSWORD = "root123"; + + private GenericContainer<?> container; + private MysqlR2dbcJdbcQueryExecutor jdbcQueryExecutor; + private List<Metrics> mariadbTemplateMetrics; + + @BeforeAll + void setUp() throws Exception { + Assumptions.assumeTrue(DockerClientFactory.instance().isDockerAvailable(), "Docker is required for integration tests"); + new CollectStrategyFactory().run(); + container = new GenericContainer<>(DockerImageName.parse("mariadb:11.4")) + .withExposedPorts(3306) + .withEnv("MARIADB_DATABASE", TEST_DATABASE) + .withEnv("MARIADB_USER", TEST_USERNAME) + .withEnv("MARIADB_PASSWORD", TEST_PASSWORD) + .withEnv("MARIADB_ROOT_PASSWORD", ROOT_PASSWORD) + .waitingFor(Wait.forListeningPort()); + container.start(); + awaitTcpLoginReady(container, TEST_USERNAME, TEST_PASSWORD, TEST_DATABASE); + initMonitoringData(container); + + MysqlCollectorProperties properties = new MysqlCollectorProperties(); + properties.setQueryEngine(MysqlCollectorProperties.QueryEngine.R2DBC); + jdbcQueryExecutor = new MysqlR2dbcJdbcQueryExecutor( + properties, + new MysqlR2dbcQueryExecutor( + new MysqlR2dbcConnectionFactoryProvider(), + new ResultSetMapper(), + new SqlGuard()), + new MysqlJdbcDriverAvailability()); + jdbcQueryExecutor.afterPropertiesSet(); + mariadbTemplateMetrics = loadMariadbTemplate().getMetrics(); + } + + @AfterAll + void tearDown() throws Exception { + if (jdbcQueryExecutor != null) { + jdbcQueryExecutor.destroy(); + } + if (container != null) { + container.stop(); + } + } + + @TestFactory + Stream<DynamicTest> shouldCollectOfficialMariadbTemplateThroughJdbcQueryAdapter() { + return mariadbTemplateMetrics.stream() + .map(templateMetric -> DynamicTest.dynamicTest(templateMetric.getName(), + () -> verifyTemplateMetric(templateMetric))); + } + + private void verifyTemplateMetric(Metrics templateMetric) throws Exception { + Metrics metric = materializeMetric(templateMetric); + if ("process_state".equals(metric.getName())) { + startBackgroundSleepQuery(container); + } + if ("slow_sql".equals(metric.getName())) { + generateSlowQuery(container); + } + CollectRep.MetricsData metricsData = collect(metric); + assertEquals(CollectRep.Code.SUCCESS, metricsData.getCode(), + () -> metric.getName() + " failed: " + metricsData.getMsg()); + assertEquals(metric.getFields().size(), metricsData.getFieldsCount(), + () -> metric.getName() + " fields should still be produced by the original parser"); + if ("columns".equals(metric.getJdbc().getQueryType())) { + assertEquals(1, metricsData.getValuesCount(), () -> metric.getName() + " should keep the original single-row shape"); + } + if ("basic".equals(metric.getName())) { + assertTrue(metricsData.getValuesCount() > 0); + assertNotNull(metricsData.getValues().getFirst().getColumns(0)); + assertTrue(!Objects.equals(CommonConstants.NULL_VALUE, metricsData.getValues().getFirst().getColumns(0)), + "basic.version should be collected through the adapted query path"); + } + if ("process_state".equals(metric.getName()) || "slow_sql".equals(metric.getName())) { + assertTrue(metricsData.getValuesCount() > 0, () -> metric.getName() + " should return at least one row"); + } + } + + private CollectRep.MetricsData collect(Metrics metric) { + Job job = Job.builder() + .monitorId(1L) + .tenantId(1L) + .app("mariadb") + .defaultInterval(600L) + .metadata(new HashMap<>(0)) + .labels(new HashMap<>(0)) + .annotations(new HashMap<>(0)) + .configmap(new ArrayList<>(0)) + .metrics(new ArrayList<>(List.of(metric))) + .build(); + WheelTimerTask timerTask = new WheelTimerTask(job, timeout -> { + }); + CapturingCollectDataDispatch collectDataDispatch = new CapturingCollectDataDispatch(); + MetricsCollect metricsCollect = new MetricsCollect( + metric, + new StubTimeout(timerTask), + collectDataDispatch, + "collector-test", + List.of()); + metricsCollect.run(); + assertNotNull(collectDataDispatch.metricsData, metric.getName() + " should dispatch metrics data"); + return collectDataDispatch.metricsData; + } + + private Metrics materializeMetric(Metrics templateMetric) { + Metrics metric = JsonUtil.fromJson(JsonUtil.toJson(templateMetric), Metrics.class); + JdbcProtocol jdbcProtocol = metric.getJdbc(); + jdbcProtocol.setHost(container.getHost()); + jdbcProtocol.setPort(String.valueOf(container.getMappedPort(3306))); + jdbcProtocol.setUsername(TEST_USERNAME); + jdbcProtocol.setPassword(TEST_PASSWORD); + jdbcProtocol.setTimeout(String.valueOf(Duration.ofSeconds(8).toMillis())); + jdbcProtocol.setReuseConnection("false"); + jdbcProtocol.setUrl(null); + jdbcProtocol.setSshTunnel(null); + if (jdbcProtocol.getDatabase() == null || jdbcProtocol.getDatabase().contains("^_^")) { + jdbcProtocol.setDatabase(TEST_DATABASE); + } + if (metric.getAliasFields() == null || metric.getAliasFields().isEmpty()) { + metric.setAliasFields(metric.getFields().stream().map(Metrics.Field::getField).collect(Collectors.toList())); + } + return metric; + } + + private Job loadMariadbTemplate() throws IOException { + Path template = Path.of("..", "..", "hertzbeat-manager", "src", "main", "resources", "define", "app-mariadb.yml") + .toAbsolutePath() + .normalize(); + Yaml yaml = new Yaml(); + try (Reader reader = Files.newBufferedReader(template)) { + return yaml.loadAs(reader, Job.class); + } + } + + private void initMonitoringData(GenericContainer<?> mariaDb) throws Exception { + execRoot(mariaDb, + "GRANT SELECT ON mysql.* TO '" + TEST_USERNAME + "'@'%';" + + " GRANT PROCESS ON *.* TO '" + TEST_USERNAME + "'@'%';" + + " SET GLOBAL log_output='TABLE';" + + " SET GLOBAL slow_query_log='ON';" + + " SET GLOBAL long_query_time=0;" + + " FLUSH PRIVILEGES;"); + generateSlowQuery(mariaDb); + } + + private void generateSlowQuery(GenericContainer<?> mariaDb) throws Exception { + execUser(mariaDb, TEST_DATABASE, "SELECT SLEEP(0.2);"); + Thread.sleep(300); + } + + private void startBackgroundSleepQuery(GenericContainer<?> mariaDb) throws Exception { + String command = String.join(" ", + "CLIENT=$(command -v mysql || command -v mariadb)", + "&&", + "nohup sh -lc", + "'$CLIENT --protocol=TCP -h127.0.0.1 -P3306", + "-u" + TEST_USERNAME, + "-p" + TEST_PASSWORD, + TEST_DATABASE, + "-e", + "\"SELECT SLEEP(15)\" >/tmp/process-state.log 2>&1'", + ">/dev/null 2>&1 &"); + mariaDb.execInContainer("sh", "-lc", command); + Thread.sleep(500); + } + + private void awaitTcpLoginReady(GenericContainer<?> mariaDb, String username, String password, String database) throws Exception { + long deadline = System.currentTimeMillis() + Duration.ofSeconds(30).toMillis(); + while (System.currentTimeMillis() < deadline) { + try { + var result = mariaDb.execInContainer("sh", "-lc", mysqlCliCommand(username, password, database, "SELECT 1")); + if (result.getExitCode() == 0) { + return; + } + } catch (Exception ignored) { + // Wait for the MariaDB entrypoint to finish bootstrapping and switch to the final TCP listener. + } + Thread.sleep(1000); + } + throw new IllegalStateException("Timed out waiting for MariaDB TCP login to become ready"); + } + + private void execRoot(GenericContainer<?> mariaDb, String sql) throws Exception { + var result = mariaDb.execInContainer("sh", "-lc", mysqlCliCommand("root", ROOT_PASSWORD, "mysql", sql)); + if (result.getExitCode() != 0) { + throw new IllegalStateException("root mysql command failed: " + result.getStderr()); + } + } + + private void execUser(GenericContainer<?> mariaDb, String database, String sql) throws Exception { + var result = mariaDb.execInContainer("sh", "-lc", mysqlCliCommand(TEST_USERNAME, TEST_PASSWORD, database, sql)); + if (result.getExitCode() != 0) { + throw new IllegalStateException("user mysql command failed: " + result.getStderr()); + } + } + + private String mysqlCliCommand(String username, String password, String database, String sql) { + return String.join(" ", + "CLIENT=$(command -v mysql || command -v mariadb)", + "&&", + "$CLIENT --protocol=TCP -h127.0.0.1 -P3306", + "-u" + username, + "-p" + password, + database, + "-e", + "\"" + sql.replace("\"", "\\\"") + "\""); + } + + private static final class CapturingCollectDataDispatch implements CollectDataDispatch { + + private CollectRep.MetricsData metricsData; + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, CollectRep.MetricsData metricsData) { + this.metricsData = metricsData; + } + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, List<CollectRep.MetricsData> metricsDataList) { + if (metricsDataList != null && !metricsDataList.isEmpty()) { + this.metricsData = metricsDataList.getFirst(); + } + } + } + + private record StubTimeout(WheelTimerTask wheelTimerTask) implements Timeout { + + @Override + public org.apache.hertzbeat.common.timer.Timer timer() { + return null; + } + + @Override + public org.apache.hertzbeat.common.timer.TimerTask task() { + return wheelTimerTask; + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean cancel() { + return false; + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcDriverAvailabilityTest.java b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcDriverAvailabilityTest.java new file mode 100644 index 0000000..9c5cabe --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcDriverAvailabilityTest.java @@ -0,0 +1,41 @@ +/* + * 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.collector.collect.database.mysql; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class MysqlJdbcDriverAvailabilityTest { + + @Test + void shouldTreatOnlyExtLibLocationsAsAutoJdbcSignal() { + assertTrue(MysqlJdbcDriverAvailability.isExtLibLocation("/opt/hertzbeat/ext-lib/mysql-connector-j-9.0.0.jar")); + assertTrue(MysqlJdbcDriverAvailability.isExtLibLocation("file:/C:/hertzbeat/ext-lib/mysql-connector-j-9.0.0.jar")); + assertFalse(MysqlJdbcDriverAvailability.isExtLibLocation("/Users/dev/.m2/repository/com/mysql/mysql-connector-j/9.0.0/mysql-connector-j-9.0.0.jar")); + assertFalse(MysqlJdbcDriverAvailability.isExtLibLocation(null)); + } + + @Test + void shouldIgnoreTestClasspathMysqlDriverWhenItIsNotFromExtLib() { + MysqlJdbcDriverAvailability availability = new MysqlJdbcDriverAvailability(); + + assertFalse(availability.hasMysqlJdbcDriver()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcQueryAdapterCompatibilityIntegrationTest.java b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcQueryAdapterCompatibilityIntegrationTest.java new file mode 100644 index 0000000..38d7265 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcQueryAdapterCompatibilityIntegrationTest.java @@ -0,0 +1,312 @@ +/* + * 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.collector.collect.database.mysql; + +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.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.hertzbeat.collector.collect.strategy.CollectStrategyFactory; +import org.apache.hertzbeat.collector.dispatch.CollectDataDispatch; +import org.apache.hertzbeat.collector.dispatch.MetricsCollect; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcConnectionFactoryProvider; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcQueryExecutor; +import org.apache.hertzbeat.collector.mysql.r2dbc.ResultSetMapper; +import org.apache.hertzbeat.collector.mysql.r2dbc.SqlGuard; +import org.apache.hertzbeat.collector.timer.WheelTimerTask; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.yaml.snakeyaml.Yaml; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MysqlJdbcQueryAdapterCompatibilityIntegrationTest { + + private static final String TEST_DATABASE = "hzb"; + private static final String TEST_USERNAME = "test"; + private static final String TEST_PASSWORD = "test123"; + private static final String ROOT_PASSWORD = "root123"; + private static final Set<String> REPRESENTATIVE_TEMPLATE_METRICS = Set.of("basic", "process_state"); + + private List<Metrics> representativeTemplateMetrics; + + @BeforeAll + void setUp() throws Exception { + Assumptions.assumeTrue(DockerClientFactory.instance().isDockerAvailable(), "Docker is required for integration tests"); + new CollectStrategyFactory().run(); + representativeTemplateMetrics = loadMysqlTemplate().getMetrics().stream() + .filter(metric -> REPRESENTATIVE_TEMPLATE_METRICS.contains(metric.getName())) + .collect(Collectors.toList()); + } + + @TestFactory + Stream<DynamicTest> shouldCollectRepresentativeTemplateMetricsAcrossCompatibilityMatrix() { + return Stream.of( + new DatabaseTarget("mysql-5.7.44", DockerImageName.parse("mysql:5.7.44"), false), + new DatabaseTarget("mysql-8.0.36", DockerImageName.parse("mysql:8.0.36"), false), + new DatabaseTarget("mariadb-11.4", DockerImageName.parse("mariadb:11.4"), true)) + .map(target -> DynamicTest.dynamicTest(target.name(), () -> verifyRepresentativeMetrics(target))); + } + + private void verifyRepresentativeMetrics(DatabaseTarget target) throws Exception { + MysqlCollectorProperties properties = new MysqlCollectorProperties(); + properties.setQueryEngine(MysqlCollectorProperties.QueryEngine.R2DBC); + MysqlR2dbcJdbcQueryExecutor jdbcQueryExecutor = new MysqlR2dbcJdbcQueryExecutor( + properties, + new MysqlR2dbcQueryExecutor( + new MysqlR2dbcConnectionFactoryProvider(), + new ResultSetMapper(), + new SqlGuard()), + new MysqlJdbcDriverAvailability()); + try (GenericContainer<?> container = createContainer(target)) { + jdbcQueryExecutor.afterPropertiesSet(); + container.start(); + awaitTcpLoginReady(container, TEST_USERNAME, TEST_PASSWORD, TEST_DATABASE); + initMonitoringData(container); + + for (Metrics templateMetric : representativeTemplateMetrics) { + Metrics metric = materializeMetric(templateMetric, container); + if ("process_state".equals(metric.getName())) { + startBackgroundSleepQuery(container); + } + CollectRep.MetricsData metricsData = collect(metric); + assertEquals(CollectRep.Code.SUCCESS, metricsData.getCode(), + () -> target.name() + " " + metric.getName() + " failed: " + metricsData.getMsg()); + assertEquals(metric.getFields().size(), metricsData.getFieldsCount(), + () -> target.name() + " " + metric.getName() + " should keep the original parser output shape"); + if ("basic".equals(metric.getName())) { + assertTrue(metricsData.getValuesCount() > 0, () -> target.name() + " basic should return data"); + assertNotNull(metricsData.getValues().getFirst().getColumns(0)); + assertTrue(!Objects.equals(CommonConstants.NULL_VALUE, metricsData.getValues().getFirst().getColumns(0)), + () -> target.name() + " basic.version should be collected"); + } + if ("process_state".equals(metric.getName())) { + assertTrue(metricsData.getValuesCount() > 0, + () -> target.name() + " process_state should return at least one grouped state row"); + } + } + } finally { + jdbcQueryExecutor.destroy(); + } + } + + private GenericContainer<?> createContainer(DatabaseTarget target) { + GenericContainer<?> container = new GenericContainer<>(target.image()) + .withExposedPorts(3306) + .waitingFor(Wait.forListeningPort()); + if (target.mariaDb()) { + return container.withEnv("MARIADB_DATABASE", TEST_DATABASE) + .withEnv("MARIADB_USER", TEST_USERNAME) + .withEnv("MARIADB_PASSWORD", TEST_PASSWORD) + .withEnv("MARIADB_ROOT_PASSWORD", ROOT_PASSWORD); + } + return container.withEnv("MYSQL_DATABASE", TEST_DATABASE) + .withEnv("MYSQL_USER", TEST_USERNAME) + .withEnv("MYSQL_PASSWORD", TEST_PASSWORD) + .withEnv("MYSQL_ROOT_PASSWORD", ROOT_PASSWORD); + } + + private CollectRep.MetricsData collect(Metrics metric) { + Job job = Job.builder() + .monitorId(1L) + .tenantId(1L) + .app("mysql") + .defaultInterval(600L) + .metadata(new HashMap<>(0)) + .labels(new HashMap<>(0)) + .annotations(new HashMap<>(0)) + .configmap(new ArrayList<>(0)) + .metrics(new ArrayList<>(List.of(metric))) + .build(); + WheelTimerTask timerTask = new WheelTimerTask(job, timeout -> { + }); + CapturingCollectDataDispatch collectDataDispatch = new CapturingCollectDataDispatch(); + MetricsCollect metricsCollect = new MetricsCollect( + metric, + new StubTimeout(timerTask), + collectDataDispatch, + "collector-test", + List.of()); + metricsCollect.run(); + return collectDataDispatch.metricsData; + } + + private Metrics materializeMetric(Metrics templateMetric, GenericContainer<?> container) { + Metrics metric = JsonUtil.fromJson(JsonUtil.toJson(templateMetric), Metrics.class); + JdbcProtocol jdbcProtocol = metric.getJdbc(); + jdbcProtocol.setHost(container.getHost()); + jdbcProtocol.setPort(String.valueOf(container.getMappedPort(3306))); + jdbcProtocol.setUsername(TEST_USERNAME); + jdbcProtocol.setPassword(TEST_PASSWORD); + jdbcProtocol.setTimeout(String.valueOf(Duration.ofSeconds(8).toMillis())); + jdbcProtocol.setReuseConnection("false"); + jdbcProtocol.setUrl(null); + jdbcProtocol.setSshTunnel(null); + if (jdbcProtocol.getDatabase() == null || jdbcProtocol.getDatabase().contains("^_^")) { + jdbcProtocol.setDatabase(TEST_DATABASE); + } + if (metric.getAliasFields() == null || metric.getAliasFields().isEmpty()) { + metric.setAliasFields(metric.getFields().stream().map(Metrics.Field::getField).collect(Collectors.toList())); + } + return metric; + } + + private Job loadMysqlTemplate() throws IOException { + Path template = Path.of("..", "..", "hertzbeat-manager", "src", "main", "resources", "define", "app-mysql.yml") + .toAbsolutePath() + .normalize(); + Yaml yaml = new Yaml(); + try (Reader reader = Files.newBufferedReader(template)) { + return yaml.loadAs(reader, Job.class); + } + } + + private void initMonitoringData(GenericContainer<?> mysql) throws Exception { + execRoot(mysql, + "GRANT SELECT ON mysql.* TO '" + TEST_USERNAME + "'@'%';" + + " GRANT PROCESS ON *.* TO '" + TEST_USERNAME + "'@'%';" + + " SET GLOBAL log_output='TABLE';" + + " SET GLOBAL slow_query_log='ON';" + + " SET GLOBAL long_query_time=0;" + + " FLUSH PRIVILEGES;"); + } + + private void startBackgroundSleepQuery(GenericContainer<?> mysql) throws Exception { + String command = String.join(" ", + "CLIENT=$(command -v mysql || command -v mariadb)", + "&&", + "nohup sh -lc", + "'$CLIENT --protocol=TCP -h127.0.0.1 -P3306", + "-u" + TEST_USERNAME, + "-p" + TEST_PASSWORD, + TEST_DATABASE, + "-e", + "\"SELECT SLEEP(15)\" >/tmp/process-state.log 2>&1'", + ">/dev/null 2>&1 &"); + mysql.execInContainer("sh", "-lc", command); + Thread.sleep(500); + } + + private void awaitTcpLoginReady(GenericContainer<?> mysql, String username, String password, String database) throws Exception { + long deadline = System.currentTimeMillis() + Duration.ofSeconds(30).toMillis(); + while (System.currentTimeMillis() < deadline) { + try { + var result = mysql.execInContainer("sh", "-lc", mysqlCliCommand(username, password, database, "SELECT 1")); + if (result.getExitCode() == 0) { + return; + } + } catch (Exception ignored) { + // Wait for the database entrypoint to finish bootstrapping and switch to the final TCP listener. + } + Thread.sleep(1000); + } + throw new IllegalStateException("Timed out waiting for MySQL-compatible TCP login to become ready"); + } + + private void execRoot(GenericContainer<?> mysql, String sql) throws Exception { + var result = mysql.execInContainer("sh", "-lc", mysqlCliCommand("root", ROOT_PASSWORD, "mysql", sql)); + if (result.getExitCode() != 0) { + throw new IllegalStateException("root mysql command failed: " + result.getStderr()); + } + } + + private String mysqlCliCommand(String username, String password, String database, String sql) { + return String.join(" ", + "CLIENT=$(command -v mysql || command -v mariadb)", + "&&", + "$CLIENT --protocol=TCP -h127.0.0.1 -P3306", + "-u" + username, + "-p" + password, + database, + "-e", + "\"" + sql.replace("\"", "\\\"") + "\""); + } + + private record DatabaseTarget(String name, DockerImageName image, boolean mariaDb) { + } + + private static final class CapturingCollectDataDispatch implements CollectDataDispatch { + + private CollectRep.MetricsData metricsData; + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, CollectRep.MetricsData metricsData) { + this.metricsData = metricsData; + } + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, List<CollectRep.MetricsData> metricsDataList) { + if (metricsDataList != null && !metricsDataList.isEmpty()) { + this.metricsData = metricsDataList.getFirst(); + } + } + } + + private record StubTimeout(WheelTimerTask wheelTimerTask) implements Timeout { + + @Override + public org.apache.hertzbeat.common.timer.Timer timer() { + return null; + } + + @Override + public org.apache.hertzbeat.common.timer.TimerTask task() { + return wheelTimerTask; + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean cancel() { + return false; + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcQueryAdapterTemplateIntegrationTest.java b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcQueryAdapterTemplateIntegrationTest.java new file mode 100644 index 0000000..3d91c71 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcQueryAdapterTemplateIntegrationTest.java @@ -0,0 +1,322 @@ +/* + * 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.collector.collect.database.mysql; + +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.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.hertzbeat.collector.collect.strategy.CollectStrategyFactory; +import org.apache.hertzbeat.collector.dispatch.CollectDataDispatch; +import org.apache.hertzbeat.collector.dispatch.MetricsCollect; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcConnectionFactoryProvider; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcQueryExecutor; +import org.apache.hertzbeat.collector.mysql.r2dbc.ResultSetMapper; +import org.apache.hertzbeat.collector.mysql.r2dbc.SqlGuard; +import org.apache.hertzbeat.collector.timer.WheelTimerTask; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestFactory; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.yaml.snakeyaml.Yaml; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MysqlJdbcQueryAdapterTemplateIntegrationTest { + + private static final String TEST_DATABASE = "hzb"; + private static final String TEST_USERNAME = "test"; + private static final String TEST_PASSWORD = "test123"; + private static final String ROOT_PASSWORD = "root123"; + + private GenericContainer<?> container; + private MysqlR2dbcJdbcQueryExecutor jdbcQueryExecutor; + private List<Metrics> mysqlTemplateMetrics; + + @BeforeAll + void setUp() throws Exception { + Assumptions.assumeTrue(DockerClientFactory.instance().isDockerAvailable(), "Docker is required for integration tests"); + new CollectStrategyFactory().run(); + container = new GenericContainer<>(DockerImageName.parse("mysql:8.0.36")) + .withExposedPorts(3306) + .withEnv("MYSQL_DATABASE", TEST_DATABASE) + .withEnv("MYSQL_USER", TEST_USERNAME) + .withEnv("MYSQL_PASSWORD", TEST_PASSWORD) + .withEnv("MYSQL_ROOT_PASSWORD", ROOT_PASSWORD) + .waitingFor(Wait.forListeningPort()); + container.start(); + awaitTcpLoginReady(container, TEST_USERNAME, TEST_PASSWORD, TEST_DATABASE); + initMonitoringData(container); + + MysqlCollectorProperties properties = new MysqlCollectorProperties(); + properties.setQueryEngine(MysqlCollectorProperties.QueryEngine.R2DBC); + jdbcQueryExecutor = new MysqlR2dbcJdbcQueryExecutor( + properties, + new MysqlR2dbcQueryExecutor( + new MysqlR2dbcConnectionFactoryProvider(), + new ResultSetMapper(), + new SqlGuard()), + new MysqlJdbcDriverAvailability()); + jdbcQueryExecutor.afterPropertiesSet(); + mysqlTemplateMetrics = loadMysqlTemplate().getMetrics(); + } + + @AfterAll + void tearDown() throws Exception { + if (jdbcQueryExecutor != null) { + jdbcQueryExecutor.destroy(); + } + if (container != null) { + container.stop(); + } + } + + @TestFactory + Stream<DynamicTest> shouldCollectOfficialMysqlTemplateThroughJdbcQueryAdapter() { + return mysqlTemplateMetrics.stream() + .map(templateMetric -> DynamicTest.dynamicTest(templateMetric.getName(), + () -> verifyTemplateMetric(templateMetric))); + } + + private void verifyTemplateMetric(Metrics templateMetric) throws Exception { + Metrics metric = materializeMetric(templateMetric); + if ("process_state".equals(metric.getName())) { + startBackgroundSleepQuery(container); + } + if ("slow_sql".equals(metric.getName())) { + generateSlowQuery(container); + } + CollectRep.MetricsData metricsData = collect(metric); + assertEquals(CollectRep.Code.SUCCESS, metricsData.getCode(), + () -> metric.getName() + " failed: " + metricsData.getMsg()); + assertEquals(metric.getFields().size(), metricsData.getFieldsCount(), + () -> metric.getName() + " fields should still be produced by the original parser"); + if ("columns".equals(metric.getJdbc().getQueryType())) { + assertEquals(1, metricsData.getValuesCount(), () -> metric.getName() + " should keep the original single-row shape"); + } + if ("basic".equals(metric.getName())) { + assertTrue(metricsData.getValuesCount() > 0); + assertNotNull(metricsData.getValues().getFirst().getColumns(0)); + assertTrue(!Objects.equals(CommonConstants.NULL_VALUE, metricsData.getValues().getFirst().getColumns(0)), + "basic.version should be collected through the adapted query path"); + } + if ("process_state".equals(metric.getName()) + || "slow_sql".equals(metric.getName()) + || "account_expiry".equals(metric.getName())) { + assertTrue(metricsData.getValuesCount() > 0, () -> metric.getName() + " should return at least one row"); + } + } + + private CollectRep.MetricsData collect(Metrics metric) { + Job job = Job.builder() + .monitorId(1L) + .tenantId(1L) + .app("mysql") + .defaultInterval(600L) + .metadata(new HashMap<>(0)) + .labels(new HashMap<>(0)) + .annotations(new HashMap<>(0)) + .configmap(new ArrayList<>(0)) + .metrics(new ArrayList<>(List.of(metric))) + .build(); + WheelTimerTask timerTask = new WheelTimerTask(job, timeout -> { + }); + CapturingCollectDataDispatch collectDataDispatch = new CapturingCollectDataDispatch(); + MetricsCollect metricsCollect = new MetricsCollect( + metric, + new StubTimeout(timerTask), + collectDataDispatch, + "collector-test", + List.of()); + metricsCollect.run(); + assertNotNull(collectDataDispatch.metricsData, metric.getName() + " should dispatch metrics data"); + return collectDataDispatch.metricsData; + } + + private Metrics materializeMetric(Metrics templateMetric) { + Metrics metric = JsonUtil.fromJson(JsonUtil.toJson(templateMetric), Metrics.class); + JdbcProtocol jdbcProtocol = metric.getJdbc(); + jdbcProtocol.setHost(container.getHost()); + jdbcProtocol.setPort(String.valueOf(container.getMappedPort(3306))); + jdbcProtocol.setUsername(TEST_USERNAME); + jdbcProtocol.setPassword(TEST_PASSWORD); + jdbcProtocol.setTimeout(String.valueOf(Duration.ofSeconds(8).toMillis())); + jdbcProtocol.setReuseConnection("false"); + jdbcProtocol.setUrl(null); + jdbcProtocol.setSshTunnel(null); + if (jdbcProtocol.getDatabase() == null || jdbcProtocol.getDatabase().contains("^_^")) { + jdbcProtocol.setDatabase(TEST_DATABASE); + } + if (metric.getAliasFields() == null || metric.getAliasFields().isEmpty()) { + metric.setAliasFields(metric.getFields().stream().map(Metrics.Field::getField).collect(Collectors.toList())); + } + return metric; + } + + private Job loadMysqlTemplate() throws IOException { + Path template = Path.of("..", "..", "hertzbeat-manager", "src", "main", "resources", "define", "app-mysql.yml") + .toAbsolutePath() + .normalize(); + Yaml yaml = new Yaml(); + try (Reader reader = Files.newBufferedReader(template)) { + return yaml.loadAs(reader, Job.class); + } + } + + private void initMonitoringData(GenericContainer<?> mysql) throws Exception { + execRoot(mysql, + "GRANT SELECT ON mysql.* TO '" + TEST_USERNAME + "'@'%';" + + " GRANT PROCESS ON *.* TO '" + TEST_USERNAME + "'@'%';" + + " SET GLOBAL log_output='TABLE';" + + " SET GLOBAL slow_query_log='ON';" + + " SET GLOBAL long_query_time=0;" + + " FLUSH PRIVILEGES;"); + generateSlowQuery(mysql); + } + + private void generateSlowQuery(GenericContainer<?> mysql) throws Exception { + execUser(mysql, TEST_DATABASE, "SELECT SLEEP(0.2);"); + Thread.sleep(300); + } + + private void startBackgroundSleepQuery(GenericContainer<?> mysql) throws Exception { + String command = String.join(" ", + "CLIENT=$(command -v mysql || command -v mariadb)", + "&&", + "nohup sh -lc", + "'$CLIENT --protocol=TCP -h127.0.0.1 -P3306", + "-u" + TEST_USERNAME, + "-p" + TEST_PASSWORD, + TEST_DATABASE, + "-e", + "\"SELECT SLEEP(15)\" >/tmp/process-state.log 2>&1'", + ">/dev/null 2>&1 &"); + mysql.execInContainer("sh", "-lc", command); + Thread.sleep(500); + } + + private void awaitTcpLoginReady(GenericContainer<?> mysql, String username, String password, String database) throws Exception { + long deadline = System.currentTimeMillis() + Duration.ofSeconds(30).toMillis(); + while (System.currentTimeMillis() < deadline) { + try { + var result = mysql.execInContainer("sh", "-lc", mysqlCliCommand(username, password, database, "SELECT 1")); + if (result.getExitCode() == 0) { + return; + } + } catch (Exception ignored) { + // Wait for the MySQL entrypoint to finish bootstrapping and switch to the final TCP listener. + } + Thread.sleep(1000); + } + throw new IllegalStateException("Timed out waiting for MySQL TCP login to become ready"); + } + + private void execRoot(GenericContainer<?> mysql, String sql) throws Exception { + var result = mysql.execInContainer("sh", "-lc", mysqlCliCommand("root", ROOT_PASSWORD, "mysql", sql)); + if (result.getExitCode() != 0) { + throw new IllegalStateException("root mysql command failed: " + result.getStderr()); + } + } + + private void execUser(GenericContainer<?> mysql, String database, String sql) throws Exception { + var result = mysql.execInContainer("sh", "-lc", mysqlCliCommand(TEST_USERNAME, TEST_PASSWORD, database, sql)); + if (result.getExitCode() != 0) { + throw new IllegalStateException("user mysql command failed: " + result.getStderr()); + } + } + + private String mysqlCliCommand(String username, String password, String database, String sql) { + return String.join(" ", + "CLIENT=$(command -v mysql || command -v mariadb)", + "&&", + "$CLIENT --protocol=TCP -h127.0.0.1 -P3306", + "-u" + username, + "-p" + password, + database, + "-e", + "\"" + sql.replace("\"", "\\\"") + "\""); + } + + private static final class CapturingCollectDataDispatch implements CollectDataDispatch { + + private CollectRep.MetricsData metricsData; + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, CollectRep.MetricsData metricsData) { + this.metricsData = metricsData; + } + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, List<CollectRep.MetricsData> metricsDataList) { + if (metricsDataList != null && !metricsDataList.isEmpty()) { + this.metricsData = metricsDataList.getFirst(); + } + } + } + + private record StubTimeout(WheelTimerTask wheelTimerTask) implements Timeout { + + @Override + public org.apache.hertzbeat.common.timer.Timer timer() { + return null; + } + + @Override + public org.apache.hertzbeat.common.timer.TimerTask task() { + return wheelTimerTask; + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean cancel() { + return false; + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcQueryParityIntegrationTest.java b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcQueryParityIntegrationTest.java new file mode 100644 index 0000000..650e6e9 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlJdbcQueryParityIntegrationTest.java @@ -0,0 +1,393 @@ +/* + * 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.collector.collect.database.mysql; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.hertzbeat.collector.collect.strategy.CollectStrategyFactory; +import org.apache.hertzbeat.collector.collect.database.query.JdbcQueryExecutorRegistry; +import org.apache.hertzbeat.collector.dispatch.CollectDataDispatch; +import org.apache.hertzbeat.collector.dispatch.MetricsCollect; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcConnectionFactoryProvider; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcQueryExecutor; +import org.apache.hertzbeat.collector.mysql.r2dbc.ResultSetMapper; +import org.apache.hertzbeat.collector.mysql.r2dbc.SqlGuard; +import org.apache.hertzbeat.collector.timer.WheelTimerTask; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.yaml.snakeyaml.Yaml; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MysqlJdbcQueryParityIntegrationTest { + + private static final String TEST_DATABASE = "hzb"; + private static final String TEST_USERNAME = "test"; + private static final String TEST_PASSWORD = "test123"; + private static final String ROOT_PASSWORD = "root123"; + private static final String PARITY_TABLE = "collector_parity_metrics"; + + private Metrics basicTemplateMetric; + + @BeforeAll + void setUp() throws Exception { + Assumptions.assumeTrue(DockerClientFactory.instance().isDockerAvailable(), "Docker is required for integration tests"); + new CollectStrategyFactory().run(); + assertDoesNotThrow(() -> Class.forName("com.mysql.cj.jdbc.Driver")); + basicTemplateMetric = loadMysqlTemplate().getMetrics().stream() + .filter(metric -> "basic".equals(metric.getName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Unable to locate the basic metric in app-mysql.yml")); + } + + @AfterEach + void clearRegisteredExecutors() throws Exception { + Field executorsField = JdbcQueryExecutorRegistry.class.getDeclaredField("EXECUTORS"); + executorsField.setAccessible(true); + @SuppressWarnings("unchecked") + CopyOnWriteArrayList<Object> executors = (CopyOnWriteArrayList<Object>) executorsField.get(null); + executors.clear(); + } + + @TestFactory + Stream<DynamicTest> shouldMatchJdbcResultsForRepresentativeMysqlQueryShapes() { + return Stream.of( + new DatabaseTarget("mysql-5.7.44", DockerImageName.parse("mysql:5.7.44")), + new DatabaseTarget("mysql-8.0.36", DockerImageName.parse("mysql:8.0.36"))) + .map(target -> DynamicTest.dynamicTest(target.name(), () -> verifyParityAcrossTarget(target))); + } + + private void verifyParityAcrossTarget(DatabaseTarget target) throws Exception { + try (GenericContainer<?> container = createContainer(target)) { + container.start(); + awaitTcpLoginReady(container, TEST_USERNAME, TEST_PASSWORD, TEST_DATABASE); + initParityData(container); + + List<Metrics> parityMetrics = List.of( + materializeMetric(basicTemplateMetric, container), + buildColumnsParityMetric(container), + buildOneRowParityMetric(container), + buildMultiRowParityMetric(container)); + + for (Metrics parityMetric : parityMetrics) { + CollectRep.MetricsData jdbcResult = collectWithJdbc(parityMetric); + CollectRep.MetricsData r2dbcResult = collectWithR2dbc(parityMetric); + + assertEquals(CollectRep.Code.SUCCESS, jdbcResult.getCode(), + () -> target.name() + " JDBC baseline failed for " + parityMetric.getName() + ": " + jdbcResult.getMsg()); + assertEquals(CollectRep.Code.SUCCESS, r2dbcResult.getCode(), + () -> target.name() + " R2DBC path failed for " + parityMetric.getName() + ": " + r2dbcResult.getMsg()); + assertEquals(jdbcResult.getFields(), r2dbcResult.getFields(), + () -> target.name() + " field set differs for " + parityMetric.getName()); + assertEquals(normalizeRows(jdbcResult), normalizeRows(r2dbcResult), + () -> target.name() + " row payload differs for " + parityMetric.getName()); + assertFalse(r2dbcResult.getValues().isEmpty(), + () -> target.name() + " " + parityMetric.getName() + " should return at least one row"); + } + } + } + + private CollectRep.MetricsData collectWithJdbc(Metrics metric) throws Exception { + clearRegisteredExecutors(); + return collect(JsonUtil.fromJson(JsonUtil.toJson(metric), Metrics.class)); + } + + private CollectRep.MetricsData collectWithR2dbc(Metrics metric) throws Exception { + clearRegisteredExecutors(); + MysqlCollectorProperties properties = new MysqlCollectorProperties(); + properties.setQueryEngine(MysqlCollectorProperties.QueryEngine.R2DBC); + MysqlR2dbcJdbcQueryExecutor jdbcQueryExecutor = new MysqlR2dbcJdbcQueryExecutor( + properties, + new MysqlR2dbcQueryExecutor( + new MysqlR2dbcConnectionFactoryProvider(), + new ResultSetMapper(), + new SqlGuard()), + new MysqlJdbcDriverAvailability()); + try { + jdbcQueryExecutor.afterPropertiesSet(); + return collect(JsonUtil.fromJson(JsonUtil.toJson(metric), Metrics.class)); + } finally { + jdbcQueryExecutor.destroy(); + clearRegisteredExecutors(); + } + } + + private CollectRep.MetricsData collect(Metrics metric) { + Job job = Job.builder() + .monitorId(1L) + .tenantId(1L) + .app("mysql") + .defaultInterval(600L) + .metadata(new HashMap<>(0)) + .labels(new HashMap<>(0)) + .annotations(new HashMap<>(0)) + .configmap(new ArrayList<>(0)) + .metrics(new ArrayList<>(List.of(metric))) + .build(); + WheelTimerTask timerTask = new WheelTimerTask(job, timeout -> { + }); + CapturingCollectDataDispatch collectDataDispatch = new CapturingCollectDataDispatch(); + MetricsCollect metricsCollect = new MetricsCollect( + metric, + new StubTimeout(timerTask), + collectDataDispatch, + "collector-test", + List.of()); + metricsCollect.run(); + return collectDataDispatch.metricsData; + } + + private Metrics buildColumnsParityMetric(GenericContainer<?> container) { + return buildMetric( + "columns-parity", + List.of(field("version"), field("max_connections"), field("character_set_server")), + List.of("version", "max_connections", "character_set_server"), + "columns", + "SHOW VARIABLES WHERE Variable_name IN ('version', 'max_connections', 'character_set_server')", + container); + } + + private Metrics buildOneRowParityMetric(GenericContainer<?> container) { + return buildMetric( + "one-row-parity", + List.of(field("answer"), field("label"), field("nullable_value")), + List.of("answer", "label", "nullable_value"), + "oneRow", + "SELECT 42 AS answer, 'adapter-parity' AS label, NULL AS nullable_value", + container); + } + + private Metrics buildMultiRowParityMetric(GenericContainer<?> container) { + return buildMetric( + "multi-row-parity", + List.of(field("metric_name"), field("metric_value"), field("metric_note")), + List.of("metric_name", "metric_value", "metric_note"), + "multiRow", + "SELECT metric_name, metric_value, metric_note FROM " + PARITY_TABLE + " ORDER BY metric_name", + container); + } + + private Metrics buildMetric(String name, List<Metrics.Field> fields, List<String> aliasFields, + String queryType, String sql, GenericContainer<?> container) { + JdbcProtocol jdbcProtocol = JdbcProtocol.builder() + .host(container.getHost()) + .port(String.valueOf(container.getMappedPort(3306))) + .platform("mysql") + .database(TEST_DATABASE) + .username(TEST_USERNAME) + .password(TEST_PASSWORD) + .timeout(String.valueOf(Duration.ofSeconds(8).toMillis())) + .queryType(queryType) + .reuseConnection("false") + .url(buildJdbcUrl(container)) + .sql(sql) + .build(); + return Metrics.builder() + .name(name) + .protocol("jdbc") + .priority((byte) 1) + .fields(fields) + .aliasFields(aliasFields) + .jdbc(jdbcProtocol) + .build(); + } + + private Metrics materializeMetric(Metrics templateMetric, GenericContainer<?> container) { + Metrics metric = JsonUtil.fromJson(JsonUtil.toJson(templateMetric), Metrics.class); + JdbcProtocol jdbcProtocol = metric.getJdbc(); + jdbcProtocol.setHost(container.getHost()); + jdbcProtocol.setPort(String.valueOf(container.getMappedPort(3306))); + jdbcProtocol.setUsername(TEST_USERNAME); + jdbcProtocol.setPassword(TEST_PASSWORD); + jdbcProtocol.setTimeout(String.valueOf(Duration.ofSeconds(8).toMillis())); + jdbcProtocol.setReuseConnection("false"); + jdbcProtocol.setUrl(buildJdbcUrl(container)); + jdbcProtocol.setSshTunnel(null); + if (jdbcProtocol.getDatabase() == null || jdbcProtocol.getDatabase().contains("^_^")) { + jdbcProtocol.setDatabase(TEST_DATABASE); + } + if (metric.getAliasFields() == null || metric.getAliasFields().isEmpty()) { + metric.setAliasFields(metric.getFields().stream().map(Metrics.Field::getField).collect(Collectors.toList())); + } + return metric; + } + + private Job loadMysqlTemplate() throws IOException { + Path template = Path.of("..", "..", "hertzbeat-manager", "src", "main", "resources", "define", "app-mysql.yml") + .toAbsolutePath() + .normalize(); + Yaml yaml = new Yaml(); + try (Reader reader = Files.newBufferedReader(template)) { + return yaml.loadAs(reader, Job.class); + } + } + + private GenericContainer<?> createContainer(DatabaseTarget target) { + return new GenericContainer<>(target.image()) + .withExposedPorts(3306) + .withEnv("MYSQL_DATABASE", TEST_DATABASE) + .withEnv("MYSQL_USER", TEST_USERNAME) + .withEnv("MYSQL_PASSWORD", TEST_PASSWORD) + .withEnv("MYSQL_ROOT_PASSWORD", ROOT_PASSWORD) + .waitingFor(Wait.forListeningPort()); + } + + private void initParityData(GenericContainer<?> mysql) throws Exception { + execRoot(mysql, + "GRANT SELECT ON mysql.* TO '" + TEST_USERNAME + "'@'%';" + + " GRANT SELECT ON " + TEST_DATABASE + ".* TO '" + TEST_USERNAME + "'@'%';" + + " DROP TABLE IF EXISTS " + TEST_DATABASE + "." + PARITY_TABLE + ";" + + " CREATE TABLE " + TEST_DATABASE + "." + PARITY_TABLE + " (" + + " metric_name VARCHAR(32) PRIMARY KEY," + + " metric_value VARCHAR(32) NOT NULL," + + " metric_note VARCHAR(32) NULL" + + " );" + + " INSERT INTO " + TEST_DATABASE + "." + PARITY_TABLE + + " (metric_name, metric_value, metric_note) VALUES" + + " ('alpha', '1', NULL)," + + " ('beta', '2', 'steady');" + + " FLUSH PRIVILEGES;"); + } + + private void awaitTcpLoginReady(GenericContainer<?> mysql, String username, String password, String database) throws Exception { + long deadline = System.currentTimeMillis() + Duration.ofSeconds(30).toMillis(); + while (System.currentTimeMillis() < deadline) { + try { + var result = mysql.execInContainer("sh", "-lc", mysqlCliCommand(username, password, database, "SELECT 1")); + if (result.getExitCode() == 0) { + return; + } + } catch (Exception ignored) { + // Wait for the MySQL entrypoint to finish bootstrapping and switch to the final TCP listener. + } + Thread.sleep(1000); + } + throw new IllegalStateException("Timed out waiting for MySQL TCP login to become ready"); + } + + private void execRoot(GenericContainer<?> mysql, String sql) throws Exception { + var result = mysql.execInContainer("sh", "-lc", mysqlCliCommand("root", ROOT_PASSWORD, "mysql", sql)); + if (result.getExitCode() != 0) { + throw new IllegalStateException("root mysql command failed: " + result.getStderr()); + } + } + + private String mysqlCliCommand(String username, String password, String database, String sql) { + return String.join(" ", + "CLIENT=$(command -v mysql || command -v mariadb)", + "&&", + "$CLIENT --protocol=TCP -h127.0.0.1 -P3306", + "-u" + username, + "-p" + password, + database, + "-e", + "\"" + sql.replace("\"", "\\\"") + "\""); + } + + private String buildJdbcUrl(GenericContainer<?> container) { + return "jdbc:mysql://%s:%d/%s?allowPublicKeyRetrieval=true&useSSL=false" + .formatted(container.getHost(), container.getMappedPort(3306), TEST_DATABASE); + } + + private List<List<String>> normalizeRows(CollectRep.MetricsData metricsData) { + return metricsData.getValues().stream() + .map(valueRow -> new ArrayList<>(valueRow.getColumnsList())) + .sorted(Comparator.comparing(row -> String.join("\u0001", row))) + .collect(Collectors.toList()); + } + + private Metrics.Field field(String name) { + return Metrics.Field.builder().field(name).type((byte) 1).build(); + } + + private static final class CapturingCollectDataDispatch implements CollectDataDispatch { + + private CollectRep.MetricsData metricsData; + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, CollectRep.MetricsData metricsData) { + this.metricsData = metricsData; + } + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, List<CollectRep.MetricsData> metricsDataList) { + if (metricsDataList != null && !metricsDataList.isEmpty()) { + this.metricsData = metricsDataList.getFirst(); + } + } + } + + private record StubTimeout(WheelTimerTask wheelTimerTask) implements Timeout { + + @Override + public org.apache.hertzbeat.common.timer.Timer timer() { + return null; + } + + @Override + public org.apache.hertzbeat.common.timer.TimerTask task() { + return wheelTimerTask; + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean cancel() { + return false; + } + } + + private record DatabaseTarget(String name, DockerImageName image) { + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlR2dbcJdbcQueryExecutorTest.java b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlR2dbcJdbcQueryExecutorTest.java new file mode 100644 index 0000000..6162d5e --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/MysqlR2dbcJdbcQueryExecutorTest.java @@ -0,0 +1,143 @@ +/* + * 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.collector.collect.database.mysql; + +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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import org.apache.hertzbeat.collector.collect.database.query.JdbcQueryRowSet; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlQueryExecutor; +import org.apache.hertzbeat.collector.mysql.r2dbc.QueryResult; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.junit.jupiter.api.Test; + +class MysqlR2dbcJdbcQueryExecutorTest { + + @Test + void shouldAutoRouteToR2dbcOnlyWhenMysqlJdbcDriverIsAbsent() { + MysqlCollectorProperties properties = new MysqlCollectorProperties(); + MysqlJdbcDriverAvailability driverAvailability = mock(MysqlJdbcDriverAvailability.class); + when(driverAvailability.hasMysqlJdbcDriver()).thenReturn(false); + MysqlR2dbcJdbcQueryExecutor executor = new MysqlR2dbcJdbcQueryExecutor( + properties, mock(MysqlQueryExecutor.class), driverAvailability); + + assertTrue(executor.supports(metrics("mysql", "columns"))); + assertTrue(executor.supports(metrics("mariadb", "columns"))); + assertTrue(executor.supports(metrics("mysql", "multiRow"))); + assertFalse(executor.supports(metrics("mysql", "runScript"))); + assertFalse(executor.supports(metrics("postgresql", "columns"))); + } + + @Test + void shouldPreferJdbcWhenMysqlJdbcDriverIsPresentInAutoMode() { + MysqlCollectorProperties properties = new MysqlCollectorProperties(); + MysqlJdbcDriverAvailability driverAvailability = mock(MysqlJdbcDriverAvailability.class); + when(driverAvailability.hasMysqlJdbcDriver()).thenReturn(true); + MysqlR2dbcJdbcQueryExecutor executor = new MysqlR2dbcJdbcQueryExecutor( + properties, mock(MysqlQueryExecutor.class), driverAvailability); + + assertFalse(executor.supports(metrics("mysql", "columns"))); + } + + @Test + void shouldHonorExplicitQueryEngineOverrides() { + MysqlCollectorProperties properties = new MysqlCollectorProperties(); + MysqlJdbcDriverAvailability driverAvailability = mock(MysqlJdbcDriverAvailability.class); + when(driverAvailability.hasMysqlJdbcDriver()).thenReturn(true); + MysqlR2dbcJdbcQueryExecutor executor = new MysqlR2dbcJdbcQueryExecutor( + properties, mock(MysqlQueryExecutor.class), driverAvailability); + + properties.setQueryEngine(MysqlCollectorProperties.QueryEngine.R2DBC); + assertTrue(executor.supports(metrics("mysql", "columns"))); + + properties.setQueryEngine(MysqlCollectorProperties.QueryEngine.JDBC); + assertFalse(executor.supports(metrics("mysql", "columns"))); + } + + @Test + void shouldExposeQueryResultsAsJdbcStyleRowSet() throws Exception { + MysqlCollectorProperties properties = new MysqlCollectorProperties(); + properties.setQueryEngine(MysqlCollectorProperties.QueryEngine.R2DBC); + MysqlQueryExecutor mysqlQueryExecutor = mock(MysqlQueryExecutor.class); + when(mysqlQueryExecutor.execute(anyString(), any())) + .thenReturn(QueryResult.builder() + .columns(List.of("Variable_name", "Value")) + .rows(List.of( + List.of("Threads_connected", "5"), + List.of("Uptime", "10"))) + .elapsedMs(11) + .rowCount(2) + .build()); + MysqlR2dbcJdbcQueryExecutor executor = new MysqlR2dbcJdbcQueryExecutor( + properties, mysqlQueryExecutor, mock(MysqlJdbcDriverAvailability.class)); + + try (JdbcQueryRowSet rowSet = executor.executeQuery(metrics("mysql", "columns"), 6000, 1000)) { + assertTrue(rowSet.next()); + assertEquals("Threads_connected", rowSet.getString(1)); + assertEquals("5", rowSet.getString(2)); + assertEquals("5", rowSet.getString("value")); + + assertTrue(rowSet.next()); + assertEquals("Uptime", rowSet.getString("variable_name")); + assertEquals("10", rowSet.getString(2)); + assertFalse(rowSet.next()); + } + } + + @Test + void shouldFailFastWhenR2dbcQueryReturnsError() { + MysqlCollectorProperties properties = new MysqlCollectorProperties(); + properties.setQueryEngine(MysqlCollectorProperties.QueryEngine.R2DBC); + MysqlQueryExecutor mysqlQueryExecutor = mock(MysqlQueryExecutor.class); + when(mysqlQueryExecutor.execute(anyString(), any())) + .thenReturn(QueryResult.builder().error("query timeout").build()); + MysqlR2dbcJdbcQueryExecutor executor = new MysqlR2dbcJdbcQueryExecutor( + properties, mysqlQueryExecutor, mock(MysqlJdbcDriverAvailability.class)); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> executor.executeQuery(metrics("mysql", "columns"), 6000, 1000)); + assertTrue(exception.getMessage().contains("query timeout")); + } + + private Metrics metrics(String platform, String queryType) { + JdbcProtocol jdbcProtocol = JdbcProtocol.builder() + .host("127.0.0.1") + .port("3306") + .platform(platform) + .database("hzb") + .username("test") + .password("test123") + .queryType(queryType) + .sql("SHOW GLOBAL STATUS") + .timeout("6000") + .build(); + Metrics metrics = new Metrics(); + metrics.setProtocol("jdbc"); + metrics.setName("status"); + metrics.setJdbc(jdbcProtocol); + return metrics; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/OceanbaseJdbcQueryAdapterIntegrationTest.java b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/OceanbaseJdbcQueryAdapterIntegrationTest.java new file mode 100644 index 0000000..e95cfa6 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/OceanbaseJdbcQueryAdapterIntegrationTest.java @@ -0,0 +1,275 @@ +/* + * 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.collector.collect.database.mysql; + +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.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.hertzbeat.collector.collect.strategy.CollectStrategyFactory; +import org.apache.hertzbeat.collector.dispatch.CollectDataDispatch; +import org.apache.hertzbeat.collector.dispatch.MetricsCollect; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcConnectionFactoryProvider; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcQueryExecutor; +import org.apache.hertzbeat.collector.mysql.r2dbc.ResultSetMapper; +import org.apache.hertzbeat.collector.mysql.r2dbc.SqlGuard; +import org.apache.hertzbeat.collector.timer.WheelTimerTask; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.yaml.snakeyaml.Yaml; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class OceanbaseJdbcQueryAdapterIntegrationTest { + + private static final String OCEANBASE_IMAGE = "oceanbase/oceanbase-ce:latest"; + private static final String OCEANBASE_USERNAME = "root@sys"; + private static final String OCEANBASE_PASSWORD = ""; + private static final String OCEANBASE_DATABASE = "oceanbase"; + + private GenericContainer<?> container; + private MysqlR2dbcJdbcQueryExecutor jdbcQueryExecutor; + private List<Metrics> oceanbaseTemplateMetrics; + + @BeforeAll + void setUp() throws Exception { + Assumptions.assumeTrue(DockerClientFactory.instance().isDockerAvailable(), "Docker is required for integration tests"); + new CollectStrategyFactory().run(); + container = new GenericContainer<>(DockerImageName.parse(OCEANBASE_IMAGE)) + .withExposedPorts(2881) + .withEnv("MODE", "MINI") + .withEnv("OB_MEMORY_LIMIT", "4096M") + .withEnv("OB_SYSTEM_MEMORY", "1024M") + .withEnv("OB_DATAFILE_SIZE", "2048M") + .withEnv("OB_LOG_DISK_SIZE", "2048M") + .withCommand("bash", "-lc", "/usr/sbin/sshd || true; /root/boot/start.sh || true; tail -f /dev/null") + .waitingFor(Wait.forListeningPort()) + .withStartupTimeout(Duration.ofMinutes(5)); + container.start(); + awaitSysLoginReady(); + + MysqlCollectorProperties properties = new MysqlCollectorProperties(); + properties.setQueryEngine(MysqlCollectorProperties.QueryEngine.R2DBC); + jdbcQueryExecutor = new MysqlR2dbcJdbcQueryExecutor( + properties, + new MysqlR2dbcQueryExecutor( + new MysqlR2dbcConnectionFactoryProvider(), + new ResultSetMapper(), + new SqlGuard()), + new MysqlJdbcDriverAvailability()); + jdbcQueryExecutor.afterPropertiesSet(); + oceanbaseTemplateMetrics = loadOceanbaseTemplate().getMetrics(); + } + + @AfterAll + void tearDown() throws Exception { + if (jdbcQueryExecutor != null) { + jdbcQueryExecutor.destroy(); + } + if (container != null) { + container.stop(); + } + } + + @TestFactory + Stream<DynamicTest> shouldCollectOfficialOceanbaseTemplateThroughJdbcQueryAdapter() { + return oceanbaseTemplateMetrics.stream() + .map(templateMetric -> DynamicTest.dynamicTest(templateMetric.getName(), + () -> verifyTemplateMetric(templateMetric))); + } + + private void verifyTemplateMetric(Metrics templateMetric) throws Exception { + Metrics metric = materializeMetric(templateMetric); + if ("process_state".equals(metric.getName())) { + startBackgroundSleepQuery(); + } + CollectRep.MetricsData metricsData = collect(metric); + assertEquals(CollectRep.Code.SUCCESS, metricsData.getCode(), + () -> metric.getName() + " failed: " + metricsData.getMsg()); + assertEquals(metric.getFields().size(), metricsData.getFieldsCount(), + () -> metric.getName() + " fields should still be produced by the original parser"); + if ("basic".equals(metric.getName())) { + assertTrue(metricsData.getValuesCount() > 0, "basic should return data"); + assertNotNull(metricsData.getValues().getFirst().getColumns(0)); + assertTrue(!Objects.equals(CommonConstants.NULL_VALUE, metricsData.getValues().getFirst().getColumns(0)), + "basic.version should be collected through the adapted query path"); + } + if ("tenant".equals(metric.getName()) || "sql".equals(metric.getName()) || "process_state".equals(metric.getName())) { + assertTrue(metricsData.getValuesCount() > 0, () -> metric.getName() + " should return at least one row"); + } + } + + private CollectRep.MetricsData collect(Metrics metric) { + Job job = Job.builder() + .monitorId(1L) + .tenantId(1L) + .app("oceanbase") + .defaultInterval(600L) + .metadata(new HashMap<>(0)) + .labels(new HashMap<>(0)) + .annotations(new HashMap<>(0)) + .configmap(new ArrayList<>(0)) + .metrics(new ArrayList<>(List.of(metric))) + .build(); + WheelTimerTask timerTask = new WheelTimerTask(job, timeout -> { + }); + CapturingCollectDataDispatch collectDataDispatch = new CapturingCollectDataDispatch(); + MetricsCollect metricsCollect = new MetricsCollect( + metric, + new StubTimeout(timerTask), + collectDataDispatch, + "collector-test", + List.of()); + metricsCollect.run(); + assertNotNull(collectDataDispatch.metricsData, metric.getName() + " should dispatch metrics data"); + return collectDataDispatch.metricsData; + } + + private Metrics materializeMetric(Metrics templateMetric) { + Metrics metric = JsonUtil.fromJson(JsonUtil.toJson(templateMetric), Metrics.class); + JdbcProtocol jdbcProtocol = metric.getJdbc(); + jdbcProtocol.setHost(container.getHost()); + jdbcProtocol.setPort(String.valueOf(container.getMappedPort(2881))); + jdbcProtocol.setUsername(OCEANBASE_USERNAME); + jdbcProtocol.setPassword(OCEANBASE_PASSWORD); + jdbcProtocol.setTimeout(String.valueOf(Duration.ofSeconds(12).toMillis())); + jdbcProtocol.setReuseConnection("false"); + jdbcProtocol.setUrl(null); + jdbcProtocol.setSshTunnel(null); + jdbcProtocol.setDatabase(OCEANBASE_DATABASE); + if (metric.getAliasFields() == null || metric.getAliasFields().isEmpty()) { + metric.setAliasFields(metric.getFields().stream().map(Metrics.Field::getField).collect(Collectors.toList())); + } + return metric; + } + + private Job loadOceanbaseTemplate() throws IOException { + Path template = Path.of("..", "..", "hertzbeat-manager", "src", "main", "resources", "define", "app-oceanbase.yml") + .toAbsolutePath() + .normalize(); + Yaml yaml = new Yaml(); + try (Reader reader = Files.newBufferedReader(template)) { + return yaml.loadAs(reader, Job.class); + } + } + + private void awaitSysLoginReady() throws Exception { + long deadline = System.currentTimeMillis() + Duration.ofMinutes(3).toMillis(); + while (System.currentTimeMillis() < deadline) { + try { + var result = container.execInContainer("sh", "-lc", obclientCommand("select 1")); + if (result.getExitCode() == 0) { + return; + } + } catch (Exception ignored) { + // Wait for OceanBase observer bootstrap to finish and accept sys tenant logins. + } + Thread.sleep(1000); + } + throw new IllegalStateException("Timed out waiting for OceanBase sys tenant login to become ready"); + } + + private void startBackgroundSleepQuery() throws Exception { + String command = "nohup sh -lc '" + obclientCommand("select sleep(15);").replace("'", "'\"'\"'") + " >/tmp/oceanbase-process-state.log 2>&1' >/dev/null 2>&1 &"; + container.execInContainer("sh", "-lc", command); + Thread.sleep(500); + } + + private String obclientCommand(String sql) { + StringBuilder command = new StringBuilder("obclient -h127.0.0.1 -P2881 -u") + .append(OCEANBASE_USERNAME) + .append(" -D") + .append(OCEANBASE_DATABASE) + .append(" -A "); + if (!OCEANBASE_PASSWORD.isEmpty()) { + command.append("-p").append(OCEANBASE_PASSWORD).append(' '); + } + command.append("-e \"").append(sql.replace("\"", "\\\"")).append('"'); + return command.toString(); + } + + private static final class CapturingCollectDataDispatch implements CollectDataDispatch { + + private CollectRep.MetricsData metricsData; + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, CollectRep.MetricsData metricsData) { + this.metricsData = metricsData; + } + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, List<CollectRep.MetricsData> metricsDataList) { + if (metricsDataList != null && !metricsDataList.isEmpty()) { + this.metricsData = metricsDataList.getFirst(); + } + } + } + + private record StubTimeout(WheelTimerTask wheelTimerTask) implements Timeout { + + @Override + public org.apache.hertzbeat.common.timer.Timer timer() { + return null; + } + + @Override + public org.apache.hertzbeat.common.timer.TimerTask task() { + return wheelTimerTask; + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean cancel() { + return false; + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/TidbJdbcQueryAdapterIntegrationTest.java b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/TidbJdbcQueryAdapterIntegrationTest.java new file mode 100644 index 0000000..0d57526 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/mysql/TidbJdbcQueryAdapterIntegrationTest.java @@ -0,0 +1,240 @@ +/* + * 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.collector.collect.database.mysql; + +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.io.IOException; +import java.io.Reader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import org.apache.hertzbeat.collector.collect.strategy.CollectStrategyFactory; +import org.apache.hertzbeat.collector.dispatch.CollectDataDispatch; +import org.apache.hertzbeat.collector.dispatch.MetricsCollect; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcConnectionFactoryProvider; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcQueryExecutor; +import org.apache.hertzbeat.collector.mysql.r2dbc.ResultSetMapper; +import org.apache.hertzbeat.collector.mysql.r2dbc.SqlGuard; +import org.apache.hertzbeat.collector.timer.WheelTimerTask; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.yaml.snakeyaml.Yaml; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TidbJdbcQueryAdapterIntegrationTest { + + private static final String TIDB_USERNAME = "root"; + private static final String TIDB_PASSWORD = ""; + private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); + + private GenericContainer<?> container; + private MysqlR2dbcJdbcQueryExecutor jdbcQueryExecutor; + private Metrics tidbBasicMetric; + + @BeforeAll + void setUp() throws Exception { + Assumptions.assumeTrue(DockerClientFactory.instance().isDockerAvailable(), "Docker is required for integration tests"); + new CollectStrategyFactory().run(); + container = new GenericContainer<>(DockerImageName.parse("pingcap/tidb:v7.5.1")) + .withCommand("--store=unistore", "--path=") + .withExposedPorts(4000, 10080) + .waitingFor(Wait.forLogMessage(".*server is running MySQL protocol.*", 1)); + container.start(); + waitForStatusEndpoint(); + + MysqlCollectorProperties properties = new MysqlCollectorProperties(); + properties.setQueryEngine(MysqlCollectorProperties.QueryEngine.R2DBC); + jdbcQueryExecutor = new MysqlR2dbcJdbcQueryExecutor( + properties, + new MysqlR2dbcQueryExecutor( + new MysqlR2dbcConnectionFactoryProvider(), + new ResultSetMapper(), + new SqlGuard()), + new MysqlJdbcDriverAvailability()); + jdbcQueryExecutor.afterPropertiesSet(); + tidbBasicMetric = loadTidbBasicMetric(); + } + + @AfterAll + void tearDown() throws Exception { + if (jdbcQueryExecutor != null) { + jdbcQueryExecutor.destroy(); + } + if (container != null) { + container.stop(); + } + } + + @Test + void shouldCollectTidbBasicMetricThroughMysqlCompatibleQueryAdapter() { + Metrics metric = materializeMetric(tidbBasicMetric); + CollectRep.MetricsData metricsData = collect(metric); + assertEquals(CollectRep.Code.SUCCESS, metricsData.getCode(), metricsData.getMsg()); + assertEquals(metric.getFields().size(), metricsData.getFieldsCount()); + assertTrue(metricsData.getValuesCount() > 0); + assertNotNull(metricsData.getValues().getFirst()); + assertTrue(metricsData.getValues().getFirst().getColumnsList().stream() + .anyMatch(value -> !Objects.equals(CommonConstants.NULL_VALUE, value) && !value.isEmpty()), + "TiDB basic should still return at least one concrete field value through the adapted query path"); + } + + private Metrics materializeMetric(Metrics templateMetric) { + Metrics metric = JsonUtil.fromJson(JsonUtil.toJson(templateMetric), Metrics.class); + JdbcProtocol jdbcProtocol = metric.getJdbc(); + jdbcProtocol.setHost(container.getHost()); + jdbcProtocol.setPort(String.valueOf(container.getMappedPort(4000))); + jdbcProtocol.setUsername(TIDB_USERNAME); + jdbcProtocol.setPassword(TIDB_PASSWORD); + jdbcProtocol.setTimeout(String.valueOf(Duration.ofSeconds(8).toMillis())); + jdbcProtocol.setReuseConnection("false"); + jdbcProtocol.setDatabase(null); + jdbcProtocol.setUrl(null); + jdbcProtocol.setSshTunnel(null); + return metric; + } + + private Metrics loadTidbBasicMetric() throws IOException { + Path template = Path.of("..", "..", "hertzbeat-manager", "src", "main", "resources", "define", "app-tidb.yml") + .toAbsolutePath() + .normalize(); + Yaml yaml = new Yaml(); + try (Reader reader = Files.newBufferedReader(template)) { + Job job = yaml.loadAs(reader, Job.class); + return job.getMetrics().stream() + .filter(metric -> "basic".equals(metric.getName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Unable to locate the basic metric in app-tidb.yml")); + } + } + + private CollectRep.MetricsData collect(Metrics metric) { + Job job = Job.builder() + .monitorId(1L) + .tenantId(1L) + .app("tidb") + .defaultInterval(600L) + .metadata(new HashMap<>(0)) + .labels(new HashMap<>(0)) + .annotations(new HashMap<>(0)) + .configmap(new ArrayList<>(0)) + .metrics(new ArrayList<>(List.of(metric))) + .build(); + WheelTimerTask timerTask = new WheelTimerTask(job, timeout -> { + }); + CapturingCollectDataDispatch collectDataDispatch = new CapturingCollectDataDispatch(); + MetricsCollect metricsCollect = new MetricsCollect( + metric, + new StubTimeout(timerTask), + collectDataDispatch, + "collector-test", + List.of()); + metricsCollect.run(); + return collectDataDispatch.metricsData; + } + + private void waitForStatusEndpoint() throws Exception { + long deadline = System.currentTimeMillis() + Duration.ofSeconds(30).toMillis(); + String statusUrl = "http://" + container.getHost() + ":" + container.getMappedPort(10080) + "/status"; + while (System.currentTimeMillis() < deadline) { + try { + HttpRequest request = HttpRequest.newBuilder(URI.create(statusUrl)) + .GET() + .timeout(Duration.ofSeconds(3)) + .build(); + HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + return; + } + } catch (Exception ignored) { + // Wait for the TiDB status endpoint to become available. + } + Thread.sleep(1000); + } + throw new IllegalStateException("Timed out waiting for TiDB status endpoint"); + } + + private static final class CapturingCollectDataDispatch implements CollectDataDispatch { + + private CollectRep.MetricsData metricsData; + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, CollectRep.MetricsData metricsData) { + this.metricsData = metricsData; + } + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, List<CollectRep.MetricsData> metricsDataList) { + if (metricsDataList != null && !metricsDataList.isEmpty()) { + this.metricsData = metricsDataList.getFirst(); + } + } + } + + private record StubTimeout(WheelTimerTask wheelTimerTask) implements Timeout { + + @Override + public org.apache.hertzbeat.common.timer.Timer timer() { + return null; + } + + @Override + public org.apache.hertzbeat.common.timer.TimerTask task() { + return wheelTimerTask; + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean cancel() { + return false; + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/sqlserver/SqlServerJdbcTemplateIntegrationTest.java b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/sqlserver/SqlServerJdbcTemplateIntegrationTest.java new file mode 100644 index 0000000..954c27e --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/sqlserver/SqlServerJdbcTemplateIntegrationTest.java @@ -0,0 +1,435 @@ +/* + * 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.collector.collect.database.sqlserver; + +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.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.hertzbeat.collector.collect.strategy.CollectStrategyFactory; +import org.apache.hertzbeat.collector.dispatch.CollectDataDispatch; +import org.apache.hertzbeat.collector.dispatch.MetricsCollect; +import org.apache.hertzbeat.collector.timer.WheelTimerTask; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicContainer; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.yaml.snakeyaml.Yaml; + +/** + * Integration test for app-sqlserver.yml against all supported SQL Server + * versions. Waits for container readiness via JDBC (same mechanism the + * collector uses) so timing is independent of the machine. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SqlServerJdbcTemplateIntegrationTest { + + private static final String SA_PASSWORD = "HertzBeat!2026"; + private static final String TEST_LOGIN = "test_login"; + private static final String TEST_PASSWORD = "TestLogin!2026"; + + private static final List<String> SUPPORTED_VERSIONS = List.of( + "2017-latest", + "2019-latest", + "2022-latest", + "2025-latest" + ); + + private record VersionedContainer( + String versionTag, GenericContainer<?> container) { + } + + private List<VersionedContainer> containers; + private List<Metrics> templateMetrics; + + @BeforeAll + void setUp() throws Exception { + Assumptions.assumeTrue( + DockerClientFactory.instance().isDockerAvailable(), + "Docker is required for integration tests"); + new CollectStrategyFactory().run(); + templateMetrics = loadTemplate().getMetrics(); + + containers = new ArrayList<>(); + for (String tag : SUPPORTED_VERSIONS) { + GenericContainer<?> c = startContainer(tag); + containers.add(new VersionedContainer(tag, c)); + } + } + + @AfterAll + void tearDown() { + if (containers != null) { + containers.forEach(vc -> vc.container().stop()); + } + } + + // ── Contract: account_expiry must exist ── + + @Test + @DisplayName("account_expiry metric group must be defined") + void accountExpiryShouldBeDefined() { + assertTrue(templateMetrics.stream() + .anyMatch(m -> "account_expiry".equals(m.getName())), + "app-sqlserver.yml must define an account_expiry metric group"); + } + + @Test + @DisplayName("account_expiry field types must match schema") + void accountExpiryFieldTypesShouldMatch() { + Metrics expiry = templateMetrics.stream() + .filter(m -> "account_expiry".equals(m.getName())) + .findFirst().orElseThrow(); + for (Metrics.Field f : expiry.getFields()) { + switch (f.getField()) { + case "login_name", "password_expired", "is_disabled" -> + assertEquals(1, f.getType(), + f.getField() + " must be type 1 (string)"); + case "days_left" -> + assertEquals(0, f.getType(), + "days_left must be type 0 (number)"); + default -> + throw new IllegalStateException( + "Unexpected field: " + f.getField()); + } + } + } + + // ── Every metric group must collect on every version ── + + @TestFactory + Stream<DynamicContainer> shouldCollectOnAllSupportedVersions() { + return containers.stream() + .map(vc -> DynamicContainer.dynamicContainer(vc.versionTag(), + templateMetrics.stream() + .map(m -> DynamicTest.dynamicTest(m.getName(), + () -> verifyMetric(m, vc))))); + } + + private void verifyMetric(Metrics tmpl, VersionedContainer vc) + throws Exception { + Metrics metric = materialize(tmpl, vc); + CollectRep.MetricsData data = collect(metric); + + assertEquals(CollectRep.Code.SUCCESS, data.getCode(), + () -> vc.versionTag() + " " + metric.getName() + + " failed: " + data.getMsg()); + assertEquals(metric.getFields().size(), data.getFieldsCount(), + () -> vc.versionTag() + " " + metric.getName() + + " field count mismatch"); + + if ("columns".equals(metric.getJdbc().getQueryType())) { + assertEquals(1, data.getValuesCount(), + () -> vc.versionTag() + " " + metric.getName() + + " should be single-row"); + } + if ("basic".equals(metric.getName())) { + assertTrue(data.getValuesCount() > 0); + assertNotNull(data.getValues().getFirst().getColumns(0), + vc.versionTag() + " basic.version should be non-null"); + } + if ("account_expiry".equals(metric.getName())) { + assertTrue(data.getValuesCount() > 0, + vc.versionTag() + + " account_expiry must return at least one row"); + } + } + + // ── Edge-case assertions on account_expiry data ── + + @TestFactory + @DisplayName("account_expiry edge cases across all versions") + Stream<DynamicTest> accountExpiryEdgeCases() { + return containers.stream() + .flatMap(vc -> Stream.of( + DynamicTest.dynamicTest( + vc.versionTag() + " sa must appear", + () -> assertSaLoginAppears(vc)), + DynamicTest.dynamicTest( + vc.versionTag() + " days_left must be integer", + () -> assertDaysLeftIsInteger(vc)), + DynamicTest.dynamicTest( + vc.versionTag() + + " password_expired must be 0 or 1", + () -> assertPasswordExpiredIsBinary(vc)), + DynamicTest.dynamicTest( + vc.versionTag() + + " is_disabled must be 0 or 1", + () -> assertIsDisabledIsBinary(vc)))); + } + + private void assertSaLoginAppears(VersionedContainer vc) throws Exception { + CollectRep.MetricsData data = collect(expiryMetric(vc)); + boolean found = false; + for (int i = 0; i < data.getValuesCount(); i++) { + if ("sa".equalsIgnoreCase( + data.getValues().get(i).getColumns(0))) { + found = true; + break; + } + } + assertTrue(found, + vc.versionTag() + " sa login must appear in account_expiry"); + } + + private void assertDaysLeftIsInteger(VersionedContainer vc) + throws Exception { + CollectRep.MetricsData data = collect(expiryMetric(vc)); + for (int i = 0; i < data.getValuesCount(); i++) { + String v = data.getValues().get(i).getColumns(1); + assertNotNull(v, + vc.versionTag() + " days_left must not be null"); + try { + Integer.parseInt(v); + } catch (NumberFormatException e) { + throw new AssertionError( + vc.versionTag() + " days_left not an integer: " + v); + } + } + } + + private void assertPasswordExpiredIsBinary(VersionedContainer vc) + throws Exception { + CollectRep.MetricsData data = collect(expiryMetric(vc)); + for (int i = 0; i < data.getValuesCount(); i++) { + String v = data.getValues().get(i).getColumns(2); + assertNotNull(v, + vc.versionTag() + " password_expired must not be null"); + assertTrue("Y".equals(v) || "N".equals(v), + vc.versionTag() + " password_expired must be Y/N: " + v); + } + } + + private void assertIsDisabledIsBinary(VersionedContainer vc) + throws Exception { + CollectRep.MetricsData data = collect(expiryMetric(vc)); + for (int i = 0; i < data.getValuesCount(); i++) { + String v = data.getValues().get(i).getColumns(3); + assertNotNull(v, + vc.versionTag() + " is_disabled must not be null"); + assertTrue("Y".equals(v) || "N".equals(v), + vc.versionTag() + " is_disabled must be Y/N: " + v); + } + } + + private Metrics expiryMetric(VersionedContainer vc) { + return materialize( + templateMetrics.stream() + .filter(m -> "account_expiry".equals(m.getName())) + .findFirst().orElseThrow(), + vc); + } + + // ── Container lifecycle ── + + private static GenericContainer<?> startContainer(String versionTag) + throws Exception { + GenericContainer<?> container = new GenericContainer<>( + DockerImageName.parse( + "mcr.microsoft.com/mssql/server:" + versionTag)) + .withExposedPorts(1433) + .withEnv("ACCEPT_EULA", "Y") + .withEnv("MSSQL_SA_PASSWORD", SA_PASSWORD) + .waitingFor(Wait.forLogMessage( + ".*SQL Server is now ready for client connections.*", + 1)); + container.start(); + awaitJdbc(container, versionTag); + + // Create a test login with CHECK_EXPIRATION=ON + String jdbcUrl = "jdbc:sqlserver://" + + container.getHost() + ":" + + container.getMappedPort(1433) + + ";encrypt=false;trustServerCertificate=true"; + try (Connection c = DriverManager.getConnection( + jdbcUrl, "sa", SA_PASSWORD); + var stmt = c.createStatement()) { + stmt.execute("CREATE LOGIN [" + TEST_LOGIN + + "] WITH PASSWORD = '" + TEST_PASSWORD + + "', CHECK_POLICY = ON, CHECK_EXPIRATION = ON"); + } + return container; + } + + /** + * Poll the container via JDBC until a login succeeds. + * Uses a generous timeout because SQL Server startup time varies + * significantly across versions and hardware. + */ + private static void awaitJdbc(GenericContainer<?> container, + String versionTag) throws Exception { + String jdbcUrl = "jdbc:sqlserver://" + + container.getHost() + ":" + + container.getMappedPort(1433) + + ";encrypt=false;trustServerCertificate=true;loginTimeout=5"; + long deadline = System.currentTimeMillis() + + Duration.ofMinutes(3).toMillis(); + while (System.currentTimeMillis() < deadline) { + try { + try (Connection c = DriverManager.getConnection( + jdbcUrl, "sa", SA_PASSWORD)) { + if (c.isValid(3)) { + return; + } + } + } catch (Exception ignored) { + // SQL Server not accepting logins yet + } + Thread.sleep(5000); + } + throw new IllegalStateException( + "Timed out waiting for " + versionTag + " JDBC connectivity"); + } + + // ── Collection helpers (same pattern as Mysql test) ── + + private CollectRep.MetricsData collect(Metrics metric) { + Job job = Job.builder() + .monitorId(1L).tenantId(1L).app("sqlserver") + .defaultInterval(600L) + .metadata(new HashMap<>(0)) + .labels(new HashMap<>(0)) + .annotations(new HashMap<>(0)) + .configmap(new ArrayList<>(0)) + .metrics(new ArrayList<>(List.of(metric))) + .build(); + WheelTimerTask timerTask = new WheelTimerTask(job, timeout -> { + }); + var dispatch = new CapturingCollectDataDispatch(); + var collector = new MetricsCollect( + metric, new StubTimeout(timerTask), dispatch, + "collector-test", List.of()); + collector.run(); + assertNotNull(dispatch.metricsData, + metric.getName() + " should dispatch metrics data"); + return dispatch.metricsData; + } + + private Metrics materialize(Metrics templateMetric, + VersionedContainer vc) { + Metrics m = JsonUtil.fromJson( + JsonUtil.toJson(templateMetric), Metrics.class); + JdbcProtocol jdbc = m.getJdbc(); + jdbc.setHost(vc.container().getHost()); + jdbc.setPort( + String.valueOf(vc.container().getMappedPort(1433))); + jdbc.setUsername("sa"); + jdbc.setPassword(SA_PASSWORD); + jdbc.setTimeout( + String.valueOf(Duration.ofSeconds(15).toMillis())); + jdbc.setReuseConnection("false"); + jdbc.setUrl(null); + jdbc.setSshTunnel(null); + if (jdbc.getDatabase() == null + || jdbc.getDatabase().contains("^_^")) { + jdbc.setDatabase("master"); + } + if (m.getAliasFields() == null + || m.getAliasFields().isEmpty()) { + m.setAliasFields( + m.getFields().stream() + .map(Metrics.Field::getField) + .collect(Collectors.toList())); + } + return m; + } + + private Job loadTemplate() throws Exception { + Path path = Path.of( + "..", "..", "hertzbeat-manager", "src", "main", + "resources", "define", "app-sqlserver.yml") + .toAbsolutePath().normalize(); + try (Reader r = Files.newBufferedReader(path)) { + return new Yaml().loadAs(r, Job.class); + } + } + + // ── inner types ── + + private static final class CapturingCollectDataDispatch + implements CollectDataDispatch { + private CollectRep.MetricsData metricsData; + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, + CollectRep.MetricsData data) { + this.metricsData = data; + } + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, + List<CollectRep.MetricsData> list) { + if (list != null && !list.isEmpty()) { + this.metricsData = list.getFirst(); + } + } + } + + private record StubTimeout(WheelTimerTask wheelTimerTask) implements Timeout { + @Override + public org.apache.hertzbeat.common.timer.Timer timer() { + return null; + } + + @Override + public org.apache.hertzbeat.common.timer.TimerTask task() { + return wheelTimerTask; + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean cancel() { + return false; + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/strategy/CollectStrategyFactoryTest.java b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/strategy/CollectStrategyFactoryTest.java new file mode 100644 index 0000000..45310f5 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/strategy/CollectStrategyFactoryTest.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.strategy; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.junit.jupiter.api.Test; + +class CollectStrategyFactoryTest { + + @Test + void shouldRegisterCommonProtocolsFromServiceLoader() throws Exception { + new CollectStrategyFactory().run(); + + assertNotNull(CollectStrategyFactory.invoke(DispatchConstants.PROTOCOL_JDBC)); + assertNotNull(CollectStrategyFactory.invoke(DispatchConstants.PROTOCOL_HTTP)); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/nativex/NativeCollectorDefaultsTest.java b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/nativex/NativeCollectorDefaultsTest.java new file mode 100644 index 0000000..811259e --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/nativex/NativeCollectorDefaultsTest.java @@ -0,0 +1,47 @@ +/* + * 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.collector.nativex; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +class NativeCollectorDefaultsTest { + + @Test + void shouldProvideNativeAutoconfigureExcludesWhenNativeImage() { + Map<String, Object> properties = NativeCollectorDefaults.defaultProperties(true); + assertEquals(NativeCollectorDefaults.NATIVE_AUTOCONFIGURE_EXCLUDES, + properties.get(NativeCollectorDefaults.AUTOCONFIGURE_EXCLUDE_PROPERTY)); + } + + @Test + void shouldProvideJvmAutoconfigureExcludesForJvmCollector() { + Map<String, Object> properties = NativeCollectorDefaults.defaultProperties(false); + assertEquals(NativeCollectorDefaults.JVM_AUTOCONFIGURE_EXCLUDES, + properties.get(NativeCollectorDefaults.AUTOCONFIGURE_EXCLUDE_PROPERTY)); + } + + @Test + void nativeAutoconfigureExcludesShouldExtendJvmExcludes() { + assertTrue(NativeCollectorDefaults.NATIVE_AUTOCONFIGURE_EXCLUDES + .startsWith(NativeCollectorDefaults.JVM_AUTOCONFIGURE_EXCLUDES)); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/pom.xml b/hertzbeat-collector/hertzbeat-collector-common/pom.xml new file mode 100644 index 0000000..7e0de3e --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/pom.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-collector-common</artifactId> + <name>${project.artifactId}</name> + + <properties> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <!-- spring --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + <scope>provided</scope> + </dependency> + <!-- remoting --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-remoting</artifactId> + </dependency> + <!-- common --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-common-core</artifactId> + </dependency> + <!-- linux ssh --> + <dependency> + <groupId>org.apache.sshd</groupId> + <artifactId>sshd-core</artifactId> + <version>${sshd-core.version}</version> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + <!--json path parser--> + <dependency> + <groupId>com.jayway.jsonpath</groupId> + <artifactId>json-path</artifactId> + </dependency> + <!-- lru hashmap --> + <dependency> + <groupId>com.googlecode.concurrentlinkedhashmap</groupId> + <artifactId>concurrentlinkedhashmap-lru</artifactId> + <version>${concurrentlinkedhashmap-lru.version}</version> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + </dependency> + <dependency> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + </dependency> + </dependencies> +</project> diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/AbstractCollect.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/AbstractCollect.java new file mode 100644 index 0000000..2251f9b --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/AbstractCollect.java @@ -0,0 +1,49 @@ +/* + * 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.collector.collect; + +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * Specific metrics collection implementation abstract class + */ +public abstract class AbstractCollect { + + /** + * Pre-check metrics + * @param metrics metric configuration + * @throws IllegalArgumentException when validation failed + */ + public abstract void preCheck(Metrics metrics) throws IllegalArgumentException; + + + /** + * Real acquisition implementation interface + * + * @param builder response builder + * @param metrics metric configuration + */ + public abstract void collect(CollectRep.MetricsData.Builder builder, Metrics metrics); + + /** + * the protocol this collect instance support + * @return protocol str + */ + public abstract String supportProtocol(); +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/AbstractConnection.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/AbstractConnection.java new file mode 100644 index 0000000..7b3ce9f --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/AbstractConnection.java @@ -0,0 +1,42 @@ +/* + * 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.collector.collect.common.cache; + +import lombok.extern.slf4j.Slf4j; + +/** + * AbstractConnection + */ +@Slf4j +public abstract class AbstractConnection<T> implements AutoCloseable { + + /** + * @return Returns the connection. + */ + public abstract T getConnection(); + + /** + * Close connection + */ + public abstract void closeConnection() throws Exception; + + @Override + public void close() throws Exception{ + closeConnection(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/CacheIdentifier.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/CacheIdentifier.java new file mode 100644 index 0000000..603ae17 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/CacheIdentifier.java @@ -0,0 +1,50 @@ +/* + * 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.collector.collect.common.cache; + +import java.util.Objects; +import lombok.Builder; +import lombok.Data; + +/** + * resource identifier in cache + */ +@Data +@Builder +public class CacheIdentifier { + + private String ip; + + private String port; + + private String username; + + private String password; + + private String customArg; + + @Override + public String toString() { + return "CacheIdentifier {" + + "ip='" + ip + '\'' + + ", port='" + port + '\'' + + ", username+password=>hash='" + Objects.hash(username, password) + '\'' + + ", customArg='" + customArg + '\'' + + '}'; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/GlobalConnectionCache.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/GlobalConnectionCache.java new file mode 100644 index 0000000..9b66996 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/GlobalConnectionCache.java @@ -0,0 +1,303 @@ +/* + * 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.collector.collect.common.cache; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; + +/** + * Singleton LRU global resource cache for client-server connections + */ +@Slf4j +public class GlobalConnectionCache { + + /** + * Default cache time: 600 seconds + */ + private static final long DEFAULT_CACHE_TIMEOUT = 600 * 1000L; + + /** + * Cache time length + */ + private static final int CACHE_TIME_LENGTH = 2; + + + /** + * Cache timeout map + */ + private final Map<Object, Long[]> timeoutMap = new ConcurrentHashMap<>(32); + + /** + * Object cache + */ + private final ConcurrentLinkedHashMap<Object, AbstractConnection<?>> cacheMap; + + private final ScheduledExecutorService scheduledExecutor; + + private final ExecutorService cleanupExecutor; + + private final ScheduledDispatchTask cleanupTask; + + /** + * Private constructor to prevent instantiation + */ + private GlobalConnectionCache() { + this(true); + } + + GlobalConnectionCache(boolean autoStart) { + cacheMap = new ConcurrentLinkedHashMap.Builder<Object, AbstractConnection<?>>() + .maximumWeightedCapacity(Integer.MAX_VALUE) + .listener((key, value) -> { + timeoutMap.remove(key); + try { + value.close(); + } catch (Exception e) { + log.error("Connection close error for key {}: {}", key, e.getMessage(), e); + } + log.info("GlobalConnectionCache discarded key: {}, value: {}.", key, value); + }) + .build(); + this.scheduledExecutor = createCacheMonitorScheduler(); + this.cleanupExecutor = createCleanupExecutor(); + this.cleanupTask = new ScheduledDispatchTask(cleanupExecutor, this::runCleanTimeoutOrUnHealthyCache); + if (autoStart) { + initCacheMonitor(); + Runtime.getRuntime().addShutdownHook(new Thread(this::destroy)); + } + } + + /** + * Holder class for lazy-loaded singleton instance + */ + private static class Holder { + private static final GlobalConnectionCache INSTANCE = new GlobalConnectionCache(); + } + + /** + * Get the singleton instance + * + * @return GlobalConnectionCache instance + */ + public static GlobalConnectionCache getInstance() { + return Holder.INSTANCE; + } + + /** + * Initialize the cache monitor for cleaning up expired connections + */ + private void initCacheMonitor() { + scheduledExecutor.scheduleWithFixedDelay(this::dispatchCleanupCache, 2, 100, TimeUnit.SECONDS); + } + + /** + * Clean and remove timeout or unhealthy cache entries + */ + void dispatchCleanupCache() { + cleanupTask.dispatch(); + } + + void beforeCleanTimeoutOrUnHealthyCacheRun() { + } + + void destroy() { + scheduledExecutor.shutdownNow(); + cleanupExecutor.shutdownNow(); + } + + private ScheduledExecutorService createCacheMonitorScheduler() { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat("connection-cache-timeout-detector-%d") + .setDaemon(true) + .build(); + return Executors.newSingleThreadScheduledExecutor(threadFactory); + } + + private ExecutorService createCleanupExecutor() { + return Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name("connection-cache-cleaner-vt-", 0) + .uncaughtExceptionHandler((thread, throwable) -> + log.error("Connection cache cleanup has uncaughtException.", throwable)) + .factory()); + } + + private void runCleanTimeoutOrUnHealthyCache() { + beforeCleanTimeoutOrUnHealthyCacheRun(); + try { + cacheMap.forEach((key, value) -> { + Long[] cacheTime = timeoutMap.get(key); + long currentTime = System.currentTimeMillis(); + if (cacheTime == null || cacheTime.length != CACHE_TIME_LENGTH + || cacheTime[0] + cacheTime[1] < currentTime) { + log.warn("[GlobalConnectionCache] Cleaning timeout cache, key {}", key); + timeoutMap.remove(key); + cacheMap.remove(key); + try { + value.close(); + } catch (Exception e) { + log.error("Clean connection close error for key {}: {}", key, e.getMessage(), e); + } + } + }); + } catch (Exception e) { + log.error("[GlobalConnectionCache] Error cleaning timeout cache: {}", e.getMessage(), e); + } + } + + /** + * Add or update cache entry + * + * @param key Cache key + * @param value Cache value + * @param timeDiff Cache time in milliseconds + */ + public void addCache(Object key, AbstractConnection<?> value, Long timeDiff) { + removeCache(key); + if (timeDiff == null) { + timeDiff = DEFAULT_CACHE_TIMEOUT; + } + cacheMap.put(key, value); + timeoutMap.put(key, new Long[]{System.currentTimeMillis(), timeDiff}); + } + + /** + * Add or update cache entry with default timeout + * + * @param key Cache key + * @param value Cache value + */ + public void addCache(Object key, AbstractConnection<?> value) { + addCache(key, value, DEFAULT_CACHE_TIMEOUT); + } + + /** + * Get cache by key + * + * @param key Cache key + * @param refreshCache Whether to refresh the cache timeout + * @return Optional containing the cached connection if present and valid + */ + public Optional<AbstractConnection<?>> getCache(Object key, boolean refreshCache) { + Long[] cacheTime = timeoutMap.get(key); + if (cacheTime == null || cacheTime.length != CACHE_TIME_LENGTH) { + log.info("[GlobalConnectionCache] Cache miss for key {}.", key); + removeCache(key); + return Optional.empty(); + } + if (cacheTime[0] + cacheTime[1] < System.currentTimeMillis()) { + log.warn("[GlobalConnectionCache] Cache entry expired for key {}.", key); + removeCache(key); + return Optional.empty(); + } + AbstractConnection<?> value = cacheMap.compute(key, (k, v) -> { + if (v == null) { + log.error("[GlobalConnectionCache] Value is null, removing key {}.", key); + timeoutMap.remove(key); + return null; + } + if (refreshCache) { + cacheTime[0] = System.currentTimeMillis(); + timeoutMap.put(key, cacheTime); + } + return v; + }); + return Optional.ofNullable(value); + } + + /** + * Remove cache by key + * + * @param key Cache key + */ + public void removeCache(Object key) { + timeoutMap.remove(key); + AbstractConnection<?> value = cacheMap.remove(key); + try { + if (value != null) { + value.close(); + } + } catch (Exception e) { + log.error("Connection close error for key {}: {}", key, e.getMessage(), e); + } + } + + private static final class ScheduledDispatchTask { + + private final ExecutorService executor; + + private final Runnable task; + + private boolean running; + + private int pendingRuns; + + private ScheduledDispatchTask(ExecutorService executor, Runnable task) { + this.executor = executor; + this.task = task; + } + + private void dispatch() { + boolean shouldSchedule; + synchronized (this) { + pendingRuns++; + shouldSchedule = !running; + if (shouldSchedule) { + running = true; + } + } + if (shouldSchedule) { + scheduleRun(); + } + } + + private void scheduleRun() { + executor.execute(this::runOnce); + } + + private void runOnce() { + try { + task.run(); + } finally { + scheduleNextIfNeeded(); + } + } + + private void scheduleNextIfNeeded() { + boolean shouldSchedule; + synchronized (this) { + pendingRuns = Math.max(0, pendingRuns - 1); + shouldSchedule = pendingRuns > 0; + if (!shouldSchedule) { + running = false; + return; + } + } + scheduleRun(); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/JdbcConnect.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/JdbcConnect.java new file mode 100644 index 0000000..ad82887 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/JdbcConnect.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.cache; + +import java.sql.Connection; +import lombok.extern.slf4j.Slf4j; + +/** + * jdbc common connection + */ +@Slf4j +public class JdbcConnect extends AbstractConnection<Connection> { + + private final Connection connection; + + public JdbcConnect(Connection connection) { + this.connection = connection; + } + + @Override + public void closeConnection() throws Exception { + if (connection != null) { + connection.close(); + } + } + + @Override + public Connection getConnection() { + return connection; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/JmxConnect.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/JmxConnect.java new file mode 100644 index 0000000..f0e0360 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/JmxConnect.java @@ -0,0 +1,47 @@ +/* + * 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.collector.collect.common.cache; + +import javax.management.remote.JMXConnector; +import lombok.extern.slf4j.Slf4j; + +/** + * jmx connect object + **/ +@Slf4j +public class JmxConnect extends AbstractConnection<JMXConnector> { + + private final JMXConnector connection; + + public JmxConnect(JMXConnector connection) { + this.connection = connection; + } + + + @Override + public void closeConnection() throws Exception { + if (connection != null) { + connection.close(); + } + } + + @Override + public JMXConnector getConnection() { + return connection; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/RedisConnect.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/RedisConnect.java new file mode 100644 index 0000000..e24be18 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/RedisConnect.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.cache; + +import io.lettuce.core.api.StatefulConnection; +import lombok.extern.slf4j.Slf4j; + +/** + * redis connection + */ +@Slf4j +public class RedisConnect extends AbstractConnection<StatefulConnection<String, String>> { + + private final StatefulConnection<String, String> connection; + + public RedisConnect(StatefulConnection<String, String> connection) { + this.connection = connection; + } + + @Override + public void closeConnection() throws Exception { + if (connection != null) { + connection.closeAsync(); + } + } + + @Override + public StatefulConnection<String, String> getConnection() { + return connection; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/SshConnect.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/SshConnect.java new file mode 100644 index 0000000..02c0834 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/SshConnect.java @@ -0,0 +1,45 @@ +/* + * 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.collector.collect.common.cache; + +import lombok.extern.slf4j.Slf4j; +import org.apache.sshd.client.session.ClientSession; + +/** + * ssh connection holder + */ +@Slf4j +public class SshConnect extends AbstractConnection<ClientSession> { + private final ClientSession clientSession; + + public SshConnect(ClientSession clientSession) { + this.clientSession = clientSession; + } + + @Override + public void closeConnection() throws Exception { + if (clientSession != null) { + clientSession.close(); + } + } + + @Override + public ClientSession getConnection() { + return clientSession; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/http/CommonHttpClient.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/http/CommonHttpClient.java new file mode 100644 index 0000000..54001cd --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/http/CommonHttpClient.java @@ -0,0 +1,277 @@ +/* + * 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.collector.collect.common.http; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.ssl.SSLContexts; + +/** + * common http client + */ +@Slf4j +public class CommonHttpClient { + + private static CloseableHttpClient httpClient; + + private static PoolingHttpClientConnectionManager connectionManager; + + private static ScheduledExecutorService scheduledExecutor; + + private static ExecutorService cleanupExecutor; + + private static ScheduledDispatchTask cleanupTask; + + private static volatile Runnable beforeCleanupHook; + + /** + * all max total connection + */ + private static final int MAX_TOTAL_CONNECTIONS = 50000; + + /** + * peer route max total connection + */ + private static final int MAX_PER_ROUTE_CONNECTIONS = 80; + + /** + * timeout for get connect from pool(ms) + */ + private static final int REQUIRE_CONNECT_TIMEOUT = 4000; + + /** + * tcp connect timeout(ms) + */ + private static final int CONNECT_TIMEOUT = 4000; + + /** + * socket read timeout(ms) + */ + private static final int SOCKET_TIMEOUT = 60000; + + /** + * validated time for idle connection. if when reuse this connection after this time, we will check it available. + */ + private static final int INACTIVITY_VALIDATED_TIME = 10000; + + /** + * ssl supported version + */ + private static final String[] SUPPORTED_SSL = {"TLSv1", "TLSv1.1", "TLSv1.2", "SSLv3"}; + + static { + try { + SSLContext sslContext = SSLContexts.createDefault(); + X509TrustManager x509TrustManager = new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) { } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + // check server ssl certificate expired + Date now = new Date(); + if (x509Certificates != null) { + for (X509Certificate certificate : x509Certificates) { + Date deadline = certificate.getNotAfter(); + if (deadline != null && now.after(deadline)) { + throw new CertificateExpiredException(); + } + } + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { return null; } + }; + sslContext.init(null, new TrustManager[]{x509TrustManager}, null); + SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(sslContext, SUPPORTED_SSL, null, new NoopHostnameVerifier()); + Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", sslFactory) + .build(); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout(REQUIRE_CONNECT_TIMEOUT) + .setConnectTimeout(CONNECT_TIMEOUT) + .setSocketTimeout(SOCKET_TIMEOUT) + // auto redirect when 301 302 response status + .setRedirectsEnabled(true) + .build(); + // connection pool + connectionManager = new PoolingHttpClientConnectionManager(registry); + connectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS); + connectionManager.setDefaultMaxPerRoute(MAX_PER_ROUTE_CONNECTIONS); + connectionManager.setValidateAfterInactivity(INACTIVITY_VALIDATED_TIME); + httpClient = HttpClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + // clean up unavailable expired connections + .evictExpiredConnections() + // clean up available but idle connections + .evictIdleConnections(100, TimeUnit.SECONDS) + .build(); + initializeCleanupExecutors(); + scheduledExecutor.scheduleWithFixedDelay(CommonHttpClient::dispatchConnectionPoolCleanup, + 40L, 40L, TimeUnit.SECONDS); + + // shutdown hook + Runtime.getRuntime().addShutdownHook(new Thread(CommonHttpClient::close)); + + } catch (Exception ignored) {} + } + + public static CloseableHttpClient getHttpClient() { + return httpClient; + } + + public static PoolingHttpClientConnectionManager getConnectionManager() { + return connectionManager; + } + + static void dispatchConnectionPoolCleanup() { + if (cleanupTask != null) { + cleanupTask.dispatch(); + } + } + + static void setConnectionManagerForTest(PoolingHttpClientConnectionManager manager) { + connectionManager = manager; + } + + static void setBeforeCleanupHookForTest(Runnable hook) { + beforeCleanupHook = hook; + } + + public static void close() { + try { + if (httpClient != null) { + httpClient.close(); + } + } catch (Exception e) { + log.error("close http client error", e); + } + if (scheduledExecutor != null) { + scheduledExecutor.shutdownNow(); + } + if (cleanupExecutor != null) { + cleanupExecutor.shutdownNow(); + } + } + + private static void initializeCleanupExecutors() { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat("http-connection-pool-cleaner-%d") + .setDaemon(true) + .build(); + scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory); + cleanupExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name("http-connection-pool-cleaner-vt-", 0) + .uncaughtExceptionHandler((thread, throwable) -> + log.error("HTTP connection pool cleanup has uncaughtException.", throwable)) + .factory()); + cleanupTask = new ScheduledDispatchTask(cleanupExecutor, CommonHttpClient::runConnectionPoolCleanup); + } + + private static void runConnectionPoolCleanup() { + Runnable hook = beforeCleanupHook; + if (hook != null) { + hook.run(); + } + if (connectionManager == null) { + return; + } + connectionManager.closeExpiredConnections(); + connectionManager.closeIdleConnections(40, TimeUnit.SECONDS); + } + + private static final class ScheduledDispatchTask { + + private final ExecutorService executor; + + private final Runnable task; + + private boolean running; + + private int pendingRuns; + + private ScheduledDispatchTask(ExecutorService executor, Runnable task) { + this.executor = executor; + this.task = task; + } + + private void dispatch() { + boolean shouldSchedule; + synchronized (this) { + pendingRuns++; + shouldSchedule = !running; + if (shouldSchedule) { + running = true; + } + } + if (shouldSchedule) { + scheduleRun(); + } + } + + private void scheduleRun() { + executor.execute(this::runOnce); + } + + private void runOnce() { + try { + task.run(); + } finally { + scheduleNextIfNeeded(); + } + } + + private void scheduleNextIfNeeded() { + boolean shouldSchedule; + synchronized (this) { + pendingRuns = Math.max(0, pendingRuns - 1); + shouldSchedule = pendingRuns > 0; + if (!shouldSchedule) { + running = false; + return; + } + } + scheduleRun(); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/CommonSshBlacklist.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/CommonSshBlacklist.java new file mode 100644 index 0000000..edbb086 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/CommonSshBlacklist.java @@ -0,0 +1,114 @@ +/* + * 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.collector.collect.common.ssh; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Command blacklist + */ +public class CommonSshBlacklist { + + private static final Set<String> BLACKLIST; + + static { + Set<String> tempSet = new HashSet<>(); + initializeDefaultBlacklist(tempSet); + BLACKLIST = Collections.unmodifiableSet(tempSet); + } + + private CommonSshBlacklist() { + // Prevent instantiation + } + + private static void initializeDefaultBlacklist(Set<String> blacklist) { + // Adding default dangerous commands to blacklist + blacklist.add("rm "); + blacklist.add("mv "); + blacklist.add("cp "); + blacklist.add("ln "); + blacklist.add("dd "); + blacklist.add("tar "); + blacklist.add("zip "); + blacklist.add("bzip2 "); + blacklist.add("bunzip2 "); + blacklist.add("xz "); + blacklist.add("unxz "); + blacklist.add("kill "); + blacklist.add("killall "); + blacklist.add("reboot"); + blacklist.add("shutdown"); + blacklist.add("poweroff"); + blacklist.add("init 0"); + blacklist.add("init 6"); + blacklist.add("telinit 0"); + blacklist.add("telinit 6"); + blacklist.add("systemctl halt"); + blacklist.add("systemctl suspend"); + blacklist.add("systemctl hibernate"); + blacklist.add("service reboot"); + blacklist.add("service shutdown"); + blacklist.add("crontab -e"); + blacklist.add("visudo"); + blacklist.add("useradd"); + blacklist.add("userdel"); + blacklist.add("usermod"); + blacklist.add("groupadd"); + blacklist.add("groupdel"); + blacklist.add("groupmod"); + blacklist.add("passwd"); + blacklist.add("su "); + blacklist.add("sudo "); + blacklist.add("mount "); + blacklist.add("parted"); + blacklist.add("mkpart"); + blacklist.add("partprobe"); + blacklist.add("iptables"); + blacklist.add("firewalld"); + blacklist.add("nft"); + blacklist.add("nc "); + blacklist.add("netcat"); + blacklist.add("ssh "); + blacklist.add("scp "); + blacklist.add("rsync"); + blacklist.add("ftp "); + blacklist.add("sftp "); + blacklist.add("telnet "); + blacklist.add("chmod "); + blacklist.add("chattr "); + blacklist.add("dd "); + blacklist.add("mknod"); + blacklist.add("losetup"); + blacklist.add("cryptsetup"); + } + + public static boolean isCommandBlacklisted(String command) { + if (command == null || command.trim().isEmpty()) { + throw new IllegalArgumentException("Command cannot be null or empty"); + } + String trimmedCommand = command.trim(); + return BLACKLIST.stream().anyMatch(trimmedCommand::contains); + } + + public static Set<String> getBlacklist() { + return BLACKLIST; + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/CommonSshClient.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/CommonSshClient.java new file mode 100644 index 0000000..03ca9b1 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/CommonSshClient.java @@ -0,0 +1,62 @@ +/* + * 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.collector.collect.common.ssh; + +import java.time.Duration; +import lombok.extern.slf4j.Slf4j; +import org.apache.sshd.client.ClientBuilder; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.kex.BuiltinDHFactories; +import org.apache.sshd.core.CoreModuleProperties; +import org.apache.sshd.server.forward.AcceptAllForwardingFilter; + +/** + * common ssh pool client + */ +@Slf4j +public class CommonSshClient { + + private static final SshClient SSH_CLIENT; + + static { + SSH_CLIENT = SshClient.setUpDefaultClient(); + // accept all server key verifier, will print warn log : Server at {} presented unverified {} key: {} + AcceptAllServerKeyVerifier verifier = AcceptAllServerKeyVerifier.INSTANCE; + SSH_CLIENT.setServerKeyVerifier(verifier); + // Reduce heartbeat overhead while keeping unresponsive sessions bounded to about 90 seconds. + CoreModuleProperties.HEARTBEAT_INTERVAL.set(SSH_CLIENT, Duration.ofSeconds(30)); + CoreModuleProperties.HEARTBEAT_NO_REPLY_MAX.set(SSH_CLIENT, 2); + CoreModuleProperties.SOCKET_KEEPALIVE.set(SSH_CLIENT, true); + // set support all KeyExchange + SSH_CLIENT.setKeyExchangeFactories(NamedFactory.setUpTransformedFactories( + false, + BuiltinDHFactories.VALUES, + ClientBuilder.DH2KEX + )); + SSH_CLIENT.setForwardingFilter(new AcceptAllForwardingFilter()); + // todo when connect AlibabaCloud ubuntu server, custom signature factories will cause error, why? + // SSH_CLIENT.setSignatureFactories(new ArrayList<>(BuiltinSignatures.VALUES)); + SSH_CLIENT.start(); + } + + public static SshClient getSshClient() { + return SSH_CLIENT; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/SshCircuitBreaker.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/SshCircuitBreaker.java new file mode 100644 index 0000000..f69c1fa --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/SshCircuitBreaker.java @@ -0,0 +1,102 @@ +/* + * 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.collector.collect.common.ssh; + +import com.google.common.base.Ticker; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.util.concurrent.Striped; +import java.io.IOException; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import lombok.extern.slf4j.Slf4j; + +/** + * Stops repeated SSH connection attempts after consecutive authentication failures. + */ +@Slf4j +final class SshCircuitBreaker { + + static final int MAX_FAILURES = 3; + static final Duration COOLDOWN = Duration.ofMinutes(5); + + private final int maxFailures; + private final Cache<Target, Integer> failureCounts; + private final Striped<Lock> connectionLocks; + + SshCircuitBreaker() { + this(MAX_FAILURES, COOLDOWN, Ticker.systemTicker()); + } + + SshCircuitBreaker(int maxFailures, Duration cooldown, Ticker ticker) { + if (maxFailures <= 0) { + throw new IllegalArgumentException("maxFailures must be positive"); + } + if (cooldown.isZero() || cooldown.isNegative()) { + throw new IllegalArgumentException("cooldown must be positive"); + } + this.maxFailures = maxFailures; + this.failureCounts = CacheBuilder.newBuilder() + .expireAfterWrite(cooldown.toNanos(), TimeUnit.NANOSECONDS) + .ticker(ticker) + .build(); + this.connectionLocks = Striped.lock(1_024); + } + + Target buildTarget(String host, String port, String username, List<String> credentials) { + return new Target(host, port, username, List.copyOf(credentials)); + } + + Lock connectionLock(Target target) { + return connectionLocks.get(target); + } + + void checkOpen(Target target) throws IOException { + Integer failureCount = failureCounts.getIfPresent(target); + if (failureCount != null && failureCount >= maxFailures) { + throw new IOException("SSH authentication is temporarily blocked for " + target.displayName() + + " after " + failureCount + " consecutive failures"); + } + } + + void recordFailure(Target target) { + int failureCount = failureCounts.asMap().merge(target, 1, Integer::sum); + if (failureCount == maxFailures) { + log.warn("SSH authentication is temporarily blocked for {} after {} consecutive failures", + target.displayName(), failureCount); + } + } + + void recordSuccess(Target target) { + failureCounts.invalidate(target); + } + + record Target(String host, String port, String username, List<String> credentials) { + + private String displayName() { + return username + "@" + host + ":" + port; + } + + @Override + public String toString() { + return displayName(); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/SshHelper.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/SshHelper.java new file mode 100644 index 0000000..7db6e3d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/SshHelper.java @@ -0,0 +1,433 @@ +/* + * 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.collector.collect.common.ssh; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection; +import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier; +import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache; +import org.apache.hertzbeat.collector.collect.common.cache.SshConnect; +import org.apache.hertzbeat.common.entity.job.protocol.SshProtocol; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.config.hosts.HostConfigEntry; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.RuntimeSshException; +import org.apache.sshd.common.SshConstants; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.springframework.util.StringUtils; + +/** + * ssh helper + */ +@Slf4j +public class SshHelper { + + private static final GlobalConnectionCache CONNECTION_COMMON_CACHE = GlobalConnectionCache.getInstance(); + private static final SshCircuitBreaker SSH_CIRCUIT_BREAKER = new SshCircuitBreaker(); + private static final ReentrantReadWriteLock CLIENT_IDENTITY_LOCK = new ReentrantReadWriteLock(true); + + public static ClientSession getConnectSession(String host, String port, String username, String password, String privateKey, + String privateKeyPassphrase, int timeout, boolean reuseConnection) + throws IOException, GeneralSecurityException { + SshProtocol sshProtocol = SshProtocol.builder() + .host(host) + .port(port) + .username(username) + .password(password) + .privateKey(privateKey) + .privateKeyPassphrase(privateKeyPassphrase) + .build(); + return getConnectSession(sshProtocol, timeout, reuseConnection, false); + } + + public static ClientSession getConnectSession(SshProtocol sshProtocol, int timeout, boolean reuseConnection, boolean useProxy) + throws IOException, GeneralSecurityException { + boolean proxyEnabled = useProxy && StringUtils.hasText(sshProtocol.getProxyHost()); + boolean cacheConnection = reuseConnection || proxyEnabled; + List<String> targetIdentity = connectionIdentity( + sshProtocol.getPassword(), sshProtocol.getPrivateKey(), sshProtocol.getPrivateKeyPassphrase()); + List<String> connectionIdentity = connectionIdentity(targetIdentity, sshProtocol, proxyEnabled); + CacheIdentifier identifier = buildCacheIdentifier( + sshProtocol.getHost(), sshProtocol.getPort(), sshProtocol.getUsername(), connectionIdentity); + SshCircuitBreaker.Target targetCircuit = SSH_CIRCUIT_BREAKER.buildTarget( + sshProtocol.getHost(), sshProtocol.getPort(), sshProtocol.getUsername(), targetIdentity); + SshCircuitBreaker.Target proxyCircuit = proxyEnabled + ? SSH_CIRCUIT_BREAKER.buildTarget( + sshProtocol.getProxyHost(), sshProtocol.getProxyPort(), sshProtocol.getProxyUsername(), + proxyIdentity(sshProtocol)) + : null; + // Keep the final ProxyJump session cached with its intermediate session chain. + if (cacheConnection) { + ClientSession clientSession = getCachedSession(identifier); + if (clientSession != null) { + return clientSession; + } + } + + Lock connectionLock = SSH_CIRCUIT_BREAKER.connectionLock(targetCircuit); + lockInterruptibly(connectionLock, "SSH connection"); + try { + if (cacheConnection) { + ClientSession clientSession = getCachedSession(identifier); + if (clientSession != null) { + return clientSession; + } + } + SSH_CIRCUIT_BREAKER.checkOpen(targetCircuit); + if (proxyCircuit != null) { + SSH_CIRCUIT_BREAKER.checkOpen(proxyCircuit); + } + SshClient sshClient = CommonSshClient.getSshClient(); + ClientSession clientSession = proxyEnabled + ? connectThroughProxy(sshClient, sshProtocol, timeout, proxyCircuit) + : connectDirect(sshClient, sshProtocol.getUsername(), sshProtocol.getHost(), + Integer.parseInt(sshProtocol.getPort()), timeout); + return authenticate(clientSession, sshProtocol.getHost(), sshProtocol.getPassword(), + sshProtocol.getPrivateKey(), sshProtocol.getPrivateKeyPassphrase(), + timeout, cacheConnection, identifier, targetCircuit); + } finally { + connectionLock.unlock(); + } + } + + private static ClientSession authenticate( + ClientSession clientSession, String host, String password, String privateKey, + String privateKeyPassphrase, int timeout, boolean cacheConnection, + CacheIdentifier identifier, SshCircuitBreaker.Target circuitTarget) + throws IOException, GeneralSecurityException { + boolean authenticated = false; + boolean identityReady = false; + try { + addIdentity(clientSession, host, password, privateKey, privateKeyPassphrase); + identityReady = true; + clientSession.auth().verify(timeout, TimeUnit.MILLISECONDS); + authenticated = true; + SSH_CIRCUIT_BREAKER.recordSuccess(circuitTarget); + if (cacheConnection) { + CONNECTION_COMMON_CACHE.addCache(identifier, new SshConnect(clientSession)); + } + return clientSession; + } catch (IOException | GeneralSecurityException | RuntimeException e) { + if (!authenticated && ((!identityReady && isCredentialSetupFailure(e)) + || isAuthenticationRejection(e))) { + SSH_CIRCUIT_BREAKER.recordFailure(circuitTarget); + } + closeSessionOnFailure(clientSession, e); + throw e; + } + } + + private static ClientSession connectDirect( + SshClient sshClient, String username, String host, int port, int timeout) throws IOException { + Lock identityLock = CLIENT_IDENTITY_LOCK.readLock(); + lockInterruptibly(identityLock, "SSH client identity"); + try { + return sshClient.connect(username, host, port) + .verify(timeout, TimeUnit.MILLISECONDS).getSession(); + } finally { + identityLock.unlock(); + } + } + + private static ClientSession connectThroughProxy( + SshClient sshClient, SshProtocol sshProtocol, int timeout, SshCircuitBreaker.Target proxyCircuit) + throws IOException, GeneralSecurityException { + Lock identityLock = CLIENT_IDENTITY_LOCK.writeLock(); + lockInterruptibly(identityLock, "SSH client identity"); + try { + SSH_CIRCUIT_BREAKER.checkOpen(proxyCircuit); + try { + ClientSession clientSession = connectThroughProxyWithIdentity(sshClient, sshProtocol, timeout); + SSH_CIRCUIT_BREAKER.recordSuccess(proxyCircuit); + return clientSession; + } catch (IOException | GeneralSecurityException | RuntimeException e) { + if (isProxyAuthenticationFailure(e)) { + SSH_CIRCUIT_BREAKER.recordFailure(proxyCircuit); + } + throw e; + } + } finally { + identityLock.unlock(); + } + } + + private static ClientSession connectThroughProxyWithIdentity( + SshClient sshClient, SshProtocol sshProtocol, int timeout) + throws IOException, GeneralSecurityException { + HostConfigEntry proxyConfig = new HostConfigEntry(); + String proxySpec = String.format("%s@%s:%d", sshProtocol.getProxyUsername(), + sshProtocol.getProxyHost(), Integer.parseInt(sshProtocol.getProxyPort())); + proxyConfig.setHostName(sshProtocol.getHost()); + proxyConfig.setHost(sshProtocol.getHost()); + proxyConfig.setPort(Integer.parseInt(sshProtocol.getPort())); + proxyConfig.setUsername(sshProtocol.getUsername()); + proxyConfig.setProxyJump(proxySpec); + + String proxyPassword = StringUtils.hasText(sshProtocol.getProxyPassword()) + ? sshProtocol.getProxyPassword() + : null; + List<KeyPair> proxyKeyPairs = List.of(); + ClientSession clientSession = null; + try { + if (proxyPassword != null) { + sshClient.addPasswordIdentity(proxyPassword); + } + if (StringUtils.hasText(sshProtocol.getProxyPrivateKey())) { + try { + proxyKeyPairs = loadKeyPairs( + sshProtocol.getProxyHost(), sshProtocol.getProxyPrivateKey(), null); + } catch (IOException | GeneralSecurityException e) { + throw new GeneralSecurityException("Failed to load proxy private key", e); + } + proxyKeyPairs.forEach(sshClient::addPublicKeyIdentity); + } + clientSession = sshClient.connect(proxyConfig) + .verify(timeout, TimeUnit.MILLISECONDS).getSession(); + if (proxyPassword != null) { + clientSession.removePasswordIdentity(proxyPassword); + } + proxyKeyPairs.forEach(clientSession::removePublicKeyIdentity); + return clientSession; + } catch (IOException | GeneralSecurityException | RuntimeException e) { + if (clientSession != null) { + closeSessionOnFailure(clientSession, e); + } + throw e; + } finally { + if (proxyPassword != null) { + sshClient.removePasswordIdentity(proxyPassword); + } + proxyKeyPairs.forEach(sshClient::removePublicKeyIdentity); + } + } + + private static void addIdentity(ClientSession clientSession, String host, String password, String privateKey, + String privateKeyPassphrase) throws IOException, GeneralSecurityException { + if (StringUtils.hasText(password)) { + clientSession.addPasswordIdentity(password); + return; + } + if (!StringUtils.hasText(privateKey)) { + return; + } + loadKeyPairs(host, privateKey, privateKeyPassphrase).forEach(clientSession::addPublicKeyIdentity); + } + + private static List<KeyPair> loadKeyPairs(String host, String privateKey, String privateKeyPassphrase) + throws IOException, GeneralSecurityException { + String resourceKey = "ssh-key-" + valueOrEmpty(host); + FilePasswordProvider passwordProvider = (session, resource, index) -> + StringUtils.hasText(privateKeyPassphrase) ? privateKeyPassphrase : null; + try (InputStream keyStream = new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8))) { + Iterable<KeyPair> keyPairs = SecurityUtils.loadKeyPairIdentities( + null, () -> resourceKey, keyStream, passwordProvider); + if (keyPairs == null) { + throw new GeneralSecurityException("Failed to load SSH private key"); + } + List<KeyPair> loadedKeyPairs = new ArrayList<>(); + keyPairs.forEach(loadedKeyPairs::add); + if (loadedKeyPairs.isEmpty()) { + throw new GeneralSecurityException("SSH private key contains no identities"); + } + return List.copyOf(loadedKeyPairs); + } + } + + private static ClientSession getCachedSession(CacheIdentifier identifier) { + Optional<AbstractConnection<?>> cacheOption = CONNECTION_COMMON_CACHE.getCache(identifier, true); + if (cacheOption.isEmpty()) { + return null; + } + ClientSession clientSession; + try { + clientSession = ((SshConnect) cacheOption.get()).getConnection(); + if (clientSession == null || clientSession.isClosed() || clientSession.isClosing()) { + CONNECTION_COMMON_CACHE.removeCache(identifier); + return null; + } + return clientSession; + } catch (Exception e) { + log.warn("Failed to validate cached SSH session for {}", identifier, e); + CONNECTION_COMMON_CACHE.removeCache(identifier); + return null; + } + } + + private static CacheIdentifier buildCacheIdentifier( + String host, String port, String username, List<String> connectionIdentity) { + return CacheIdentifier.builder() + .ip(host) + .port(port) + .username(username) + .customArg(fingerprint(connectionIdentity)) + .build(); + } + + private static List<String> connectionIdentity( + String password, String privateKey, String privateKeyPassphrase) { + List<String> identity = new ArrayList<>(); + addAuthenticationIdentity(identity, password, privateKey, privateKeyPassphrase); + return List.copyOf(identity); + } + + private static List<String> connectionIdentity( + List<String> targetIdentity, SshProtocol sshProtocol, boolean proxyEnabled) { + List<String> identity = new ArrayList<>(targetIdentity); + if (proxyEnabled) { + identity.add("proxy"); + identity.add(valueOrEmpty(sshProtocol.getProxyHost())); + identity.add(valueOrEmpty(sshProtocol.getProxyPort())); + identity.add(valueOrEmpty(sshProtocol.getProxyUsername())); + identity.add(textOrEmpty(sshProtocol.getProxyPassword())); + identity.add(textOrEmpty(sshProtocol.getProxyPrivateKey())); + } + return List.copyOf(identity); + } + + private static List<String> proxyIdentity(SshProtocol sshProtocol) { + List<String> identity = new ArrayList<>(); + if (StringUtils.hasText(sshProtocol.getProxyPassword())) { + identity.add("proxy-password"); + identity.add(sshProtocol.getProxyPassword()); + } + if (StringUtils.hasText(sshProtocol.getProxyPrivateKey())) { + identity.add("proxy-private-key"); + identity.add(sshProtocol.getProxyPrivateKey()); + } + if (identity.isEmpty()) { + identity.add("proxy-default"); + } + return List.copyOf(identity); + } + + private static void addAuthenticationIdentity( + List<String> identity, String password, String privateKey, String privateKeyPassphrase) { + if (StringUtils.hasText(password)) { + identity.add("target-password"); + identity.add(password); + } else if (StringUtils.hasText(privateKey)) { + identity.add("target-private-key"); + identity.add(privateKey); + identity.add(textOrEmpty(privateKeyPassphrase)); + } else { + identity.add("target-default"); + } + } + + private static String fingerprint(List<String> connectionIdentity) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + for (String component : connectionIdentity) { + byte[] bytes = component.getBytes(StandardCharsets.UTF_8); + digest.update(ByteBuffer.allocate(Integer.BYTES).putInt(bytes.length).array()); + digest.update(bytes); + } + return HexFormat.of().formatHex(digest.digest()); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is unavailable", e); + } + } + + private static String valueOrEmpty(String value) { + return value == null ? "" : value; + } + + private static String textOrEmpty(String value) { + return StringUtils.hasText(value) ? value : ""; + } + + private static boolean isCredentialSetupFailure(Exception exception) { + return !(exception instanceof InterruptedIOException) + && (exception instanceof IOException || exception instanceof GeneralSecurityException); + } + + private static boolean isProxyAuthenticationFailure(Exception exception) { + return exception instanceof GeneralSecurityException || isAuthenticationRejection(exception); + } + + private static boolean isAuthenticationRejection(Exception exception) { + Throwable current = exception; + while (current != null) { + if (current instanceof InterruptedIOException) { + return false; + } + if (current instanceof SshException sshException) { + int disconnectCode = sshException.getDisconnectCode(); + return disconnectCode == SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE + || disconnectCode == SshConstants.SSH2_DISCONNECT_HOST_AUTHENTICATION_FAILED + || disconnectCode == SshConstants.SSH2_DISCONNECT_ILLEGAL_USER_NAME; + } + current = current instanceof RuntimeSshException ? current.getCause() : null; + } + return false; + } + + private static void lockInterruptibly(Lock lock, String operation) throws InterruptedIOException { + try { + lock.lockInterruptibly(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + InterruptedIOException exception = new InterruptedIOException(operation + " interrupted"); + exception.initCause(e); + throw exception; + } + } + + private static void closeSessionOnFailure(ClientSession clientSession, Exception originalException) { + if (clientSession == null) { + return; + } + if (originalException instanceof InterruptedIOException) { + try { + clientSession.close(true); + } catch (RuntimeException closeException) { + originalException.addSuppressed(closeException); + } finally { + Thread.currentThread().interrupt(); + } + return; + } + try { + clientSession.close(); + } catch (Exception closeException) { + if (closeException instanceof InterruptedIOException) { + Thread.currentThread().interrupt(); + } + originalException.addSuppressed(closeException); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/SshTunnelHelper.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/SshTunnelHelper.java new file mode 100644 index 0000000..de65033 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/SshTunnelHelper.java @@ -0,0 +1,302 @@ +/* + * 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.collector.collect.common.ssh; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.job.SshTunnel; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker; +import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.net.ServerSocket; +import java.security.GeneralSecurityException; +import java.util.Comparator; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; + +/** + * Ssh Tunnel Helper + */ +@Slf4j +public class SshTunnelHelper { + + private static final long DEFAULT_CACHE_TIMEOUT = 500 * 1000; + + private static final Map<SshClientSessionWrapper, LocalPortForwardingWrapper> TRACKER_CACHE = new ConcurrentHashMap<>(); + + + /** + * check ssh tunnel param + * + * @param sshTunnel ssh tunnel param + */ + public static void checkTunnelParam(SshTunnel sshTunnel) { + if (sshTunnel == null || !Boolean.parseBoolean(sshTunnel.getEnable())) { + return; + } + if (!StringUtils.hasText(sshTunnel.getHost())) { + throw new IllegalArgumentException("ssh tunnel must has ssh host param"); + } + if (!StringUtils.hasText(sshTunnel.getPort())) { + throw new IllegalArgumentException("ssh tunnel must has ssh port param"); + } + if (!StringUtils.hasText(sshTunnel.getUsername())) { + throw new IllegalArgumentException("ssh tunnel must has ssh username param"); + } + + } + + /** + * create ssh tunnel + * + * @param sshTunnel ssh tunnel param + * @param remoteHost remote host + * @param remotePort remote port + * @return local port + */ + public static int localPortForward(SshTunnel sshTunnel, String remoteHost, String remotePort) throws GeneralSecurityException, IOException { + boolean shareConnection = Boolean.parseBoolean(sshTunnel.getShareConnection()); + // 1. get ssh session + ClientSession session = SshHelper.getConnectSession(sshTunnel.getHost(), sshTunnel.getPort(), + sshTunnel.getUsername(), sshTunnel.getPassword(), sshTunnel.getPrivateKey(), sshTunnel.getPrivateKeyPassphrase(), + Integer.parseInt(sshTunnel.getTimeout()), shareConnection); + SshClientSessionWrapper sessionWrapper = new SshClientSessionWrapper(session, shareConnection); + + // 2. get tunnel + LocalPortForwardingWrapper forwardingWrapper = selectWrapper( + TRACKER_CACHE.get(sessionWrapper), sessionWrapper, remoteHost, remotePort); + int localPort; + if (forwardingWrapper == null) { + localPort = getRandomPort(); + LocalPortForwardingWrapper newForwardingWrapper = sessionWrapper + .createLocalPortForwardingTracker(localPort, remoteHost, Integer.parseInt(remotePort)); + TRACKER_CACHE.putIfAbsent(sessionWrapper, newForwardingWrapper); + log.info("[SSH Tunnel] created ssh forwarding tracker ssh:{}, remote:{}, localPort:{}", + sshTunnel.getHost() + ":" + sshTunnel.getPort(), remoteHost + ":" + remotePort, localPort); + } else { + localPort = forwardingWrapper.getTracker().getLocalAddress().getPort(); + } + + return localPort; + } + + /** + * get tunnel + * + * @param wrapper LocalPortForwardingWrapper + * @param sessionWrapper SshClientSessionWrapper + * @param remoteHost remote host + * @param remotePort remote port + * @return LocalPortForwardingWrapper + */ + private static LocalPortForwardingWrapper selectWrapper(LocalPortForwardingWrapper wrapper, SshClientSessionWrapper sessionWrapper, + String remoteHost, String remotePort) { + if (wrapper == null) { + return null; + } + List<LocalPortForwardingWrapper> selectList = wrapper.select(sessionWrapper.getClientSession(), localPortForwardWrapper -> { + if (!localPortForwardWrapper.isOpen()) { + return false; + } + ExplicitPortForwardingTracker tracker = localPortForwardWrapper.getTracker(); + SshdSocketAddress remoteAddress = tracker.getRemoteAddress(); + return Objects.equals(remoteAddress.getHostName(), remoteHost) + && Objects.equals(remoteAddress.getPort(), Integer.parseInt(remotePort)); + }); + + if (selectList.isEmpty()) { + return null; + } + LocalPortForwardingWrapper selected; + if (selectList.size() == 1) { + selected = selectList.get(0); + } else { + selected = selectList.stream().min(Comparator.comparing(LocalPortForwardingWrapper::getLastAccessTime)).get(); + } + selected.setLastAccessTime(System.currentTimeMillis()); + return selected; + } + + + private static int getRandomPort() throws IOException { + try (ServerSocket serverSocket = new ServerSocket(0)) { + return serverSocket.getLocalPort(); + } + } + + private static void removeSessionCacheEntry(ClientSession session) { + TRACKER_CACHE.entrySet().removeIf(entry -> { + if (!Objects.equals(entry.getKey().getClientSession(), session)) { + return false; + } + if (!entry.getKey().isShareConnection()) { + try { + entry.getKey().close(); + log.info("[SSH Tunnel] close unshared ssh connection, {}", entry.getKey()); + } catch (IOException e) { + log.error("[SSH Tunnel] close unshared ssh connection error", e); + } + } + return true; + }); + } + + @Getter + @Setter + @EqualsAndHashCode + private static class SshClientSessionWrapper { + private ClientSession clientSession; + private boolean shareConnection; + + public SshClientSessionWrapper(ClientSession clientSession, boolean shareConnection) { + this.clientSession = clientSession; + this.shareConnection = shareConnection; + } + + /** + * Starts a local port forwarding + * @param localPort local port + * @param remoteHost remove host + * @param remotePort remote port + * @return LocalPortForwardingWrapper + */ + public LocalPortForwardingWrapper createLocalPortForwardingTracker(Integer localPort, String remoteHost, Integer remotePort) throws IOException { + SshdSocketAddress remoteAddress = new SshdSocketAddress(remoteHost, remotePort); + SshdSocketAddress localAddress = new SshdSocketAddress("localhost", localPort); + ExplicitPortForwardingTracker tracker = clientSession.createLocalPortForwardingTracker(localAddress, remoteAddress); + return new LocalPortForwardingWrapper(tracker); + } + + /** + * close client session + */ + public void close() throws IOException { + clientSession.close(); + } + + @Override + public String toString() { + return "{ ssh:%s, shareConnection:%b }".formatted(clientSession, shareConnection); + } + } + + @Getter + @Setter + @EqualsAndHashCode + private static class LocalPortForwardingWrapper { + private static Map<ClientSession, List<LocalPortForwardingWrapper>> map = new ConcurrentHashMap<>(); + + private ExplicitPortForwardingTracker tracker; + private Long lastAccessTime; + + public LocalPortForwardingWrapper(ExplicitPortForwardingTracker tracker) { + this.tracker = tracker; + this.lastAccessTime = System.currentTimeMillis(); + map.computeIfAbsent(tracker.getClientSession(), (key) -> new ArrayList<>()).add(this); + } + + /** + * select ClientSession LocalPortForwardingWrapper List + * @param session ssh client session + * @param predicate condition + * @return LocalPortForwardWrapper + */ + public List<LocalPortForwardingWrapper> select(ClientSession session, Predicate<LocalPortForwardingWrapper> predicate) { + List<LocalPortForwardingWrapper> trackerList = map.get(session); + if (CollectionUtils.isEmpty(trackerList)) { + return trackerList; + } + List<LocalPortForwardingWrapper> list = new ArrayList<>(); + long currentTimeMillis = System.currentTimeMillis(); + Iterator<LocalPortForwardingWrapper> iterator = trackerList.iterator(); + while (iterator.hasNext()) { + LocalPortForwardingWrapper wrapper = iterator.next(); + // lazy remove + if (currentTimeMillis - wrapper.getLastAccessTime() > DEFAULT_CACHE_TIMEOUT) { + try { + wrapper.getTracker().close(); + iterator.remove(); + log.info("[SSH Tunnel] Lazy Remove ssh local port forwarding {}", wrapper); + } catch (IOException e) { + log.warn("[SSH Tunnel] Lazy Remove ssh local port forwarding Error", e); + } + } else if (predicate == null || predicate.test(wrapper)) { + list.add(wrapper); + } + } + if (trackerList.isEmpty()) { + map.remove(session); + removeSessionCacheEntry(session); + } + return list; + } + + /** + * remove session local port forwarding + * @param session ssh client session + */ + public void remove(ClientSession session) { + List<LocalPortForwardingWrapper> trackerList = map.get(session); + if (CollectionUtils.isEmpty(trackerList)) { + return; + } + Iterator<LocalPortForwardingWrapper> iterator = trackerList.iterator(); + while (iterator.hasNext()){ + try { + LocalPortForwardingWrapper next = iterator.next(); + next.close(); + iterator.remove(); + log.info("[SSH Tunnel] Remove ssh local port forwarding, {}", next); + } catch (IOException e) { + log.error("[SSH Tunnel] Remove ssh session local port forwarding error", e); + } + } + map.remove(session); + removeSessionCacheEntry(session); + } + + public void close() throws IOException { + tracker.close(); + } + + public boolean isOpen() { + return tracker.isOpen(); + } + + @Override + public String toString() { + return "{ ssh:%s, remote:%s, localPort:%d }".formatted( + tracker.getSession().getConnectAddress(), + tracker.getRemoteAddress(), + tracker.getLocalAddress().getPort() + ); + } + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/config/CollectorAutoConfiguration.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/config/CollectorAutoConfiguration.java new file mode 100644 index 0000000..0d5ccda --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/config/CollectorAutoConfiguration.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.config; + +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.ComponentScan; + +/** + * Collector Auto Configuration + * @version 2.1 + */ + +@AutoConfiguration +@ComponentScan(basePackages = ConfigConstants.PkgConstant.PKG + + SignConstants.DOT + + ConfigConstants.FunctionModuleConstants.COLLECTOR) +public class CollectorAutoConfiguration { +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/constants/CollectorConstants.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/constants/CollectorConstants.java new file mode 100644 index 0000000..32d7b29 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/constants/CollectorConstants.java @@ -0,0 +1,76 @@ +/* + * 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.collector.constants; + +import org.apache.hertzbeat.common.constants.NetworkConstants; + +/** + * collector module constant. + * Extends {@link NetworkConstants} + */ + +public interface CollectorConstants extends NetworkConstants { + + String KEYWORD = "keyword"; + + /** + * POSTGRESQL un reachable status code + */ + String POSTGRESQL_UN_REACHABLE_CODE = "08001"; + + /** + * MongoDB Atlas model + */ + String MONGO_DB_ATLAS_MODEL = "mongodb-atlas"; + + String ZOOKEEPER_APP = "zookeeper"; + + String ZOOKEEPER_ENVI_HEAD = "Environment:"; + + String ERROR_MSG = "errorMsg"; + + + String RESPONSE_TIME = "responseTime"; + + String STATUS_CODE = "statusCode"; + + /** + * Maximum XML response size in bytes (10MB) to prevent DoS attacks + */ + int MAX_XML_RESPONSE_SIZE = 10 * 1024 * 1024; + + /** + * Maximum number of nodes returned by Xpath query to prevent excessive resource consumption + */ + int MAX_XPATH_RESULT_NODES = 1000; + + /** + * Dangerous Xpath expression patterns that could cause DoS attacks + * These patterns match expressions that traverse the entire XML document + */ + String[] DANGEROUS_XPATH_PATTERNS = { + "//\\*\\s*\\|\\s*//@\\*\\s*\\|\\s*//text\\(\\)", // //* | //@* | //text() + "//\\*\\s*\\|", // //* | ... + "//@\\*\\s*\\|", // //@* | ... + "//node\\(\\)\\s*\\|", // //node() | ... + "descendant-or-self::node\\(\\)\\s*\\|", // descendant-or-self::node() | ... + "/descendant-or-self::node\\(\\)", // /descendant-or-self::node() + "//\\*[\\s\\S]*//\\*" // //** with multiple wildcards + }; + +} \ No newline at end of file diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/constants/ScheduleTypeEnum.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/constants/ScheduleTypeEnum.java new file mode 100644 index 0000000..c8ecde1 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/constants/ScheduleTypeEnum.java @@ -0,0 +1,39 @@ +/* + * 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.collector.constants; + +import lombok.Getter; + +/** + * Schedule Type + */ +@Getter +public enum ScheduleTypeEnum { + + INTERVAL("interval"), + CRON("cron"); + + private final String type; + + ScheduleTypeEnum(String type) { + this.type = type; + } + + + +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/CollectDataDispatch.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/CollectDataDispatch.java new file mode 100644 index 0000000..f4f4e14 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/CollectDataDispatch.java @@ -0,0 +1,48 @@ +/* + * 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.collector.dispatch; + +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +import java.util.List; + +/** + * Collection data scheduler interface + */ +public interface CollectDataDispatch { + + /** + * Processing and distributing collection result data + * @param timeout time wheel timeout + * @param metrics The following metrics collection tasks + * @param metricsData Collect result data + */ + void dispatchCollectData(Timeout timeout, Metrics metrics, CollectRep.MetricsData metricsData); + + /** + * Processing and distributing collection result data + * + * @param timeout time wheel timeout + * @param metrics The following metrics collection tasks + * @param metricsDataList Collect result data + */ + void dispatchCollectData(Timeout timeout, Metrics metrics, List<CollectRep.MetricsData> metricsDataList); + +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/CollectorInfoProperties.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/CollectorInfoProperties.java new file mode 100644 index 0000000..233908a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/CollectorInfoProperties.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.dispatch; + +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Collector info configuration Properties + */ +@Component +@ConfigurationProperties(prefix = ConfigConstants.FunctionModuleConstants.COLLECTOR + + SignConstants.DOT + + ConfigConstants.FunctionModuleConstants.INFO) +public class CollectorInfoProperties { + + private String version; + private String ip; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/DispatchConstants.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/DispatchConstants.java new file mode 100644 index 0000000..b01a0ce --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/DispatchConstants.java @@ -0,0 +1,256 @@ +/* + * 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.collector.dispatch; + +/** + * dispatch constant + */ +public interface DispatchConstants { + + /** + * protocol http + */ + String PROTOCOL_HTTP = "http"; + + /** + * protocol icmp + */ + String PROTOCOL_ICMP = "icmp"; + /** + * protocol telnet + */ + String PROTOCOL_TELNET = "telnet"; + /** + * protocol nginx + */ + String PROTOCOL_NGINX = "nginx"; + /** + * protocol smtp + */ + String PROTOCOL_SMTP = "smtp"; + /** + * protocol ntp + */ + String PROTOCOL_NTP = "ntp"; + /** + * protocol websocket + */ + String PROTOCOL_WEBSOCKET = "websocket"; + /** + * protocol memcached + */ + String PROTOCOL_MEMCACHED = "memcached"; + /** + * protocol nebulagraph + */ + String PROTOCOL_NEBULAGRAPH = "nebulaGraph"; + /** + * protocol ngql + */ + String PROTOCOL_NGQL = "ngql"; + /** + * protocol udp + */ + String PROTOCOL_UDP = "udp"; + /** + * protocol jdbc + */ + String PROTOCOL_JDBC = "jdbc"; + /** + * protocol ssh + */ + String PROTOCOL_SSH = "ssh"; + /** + * protocol redis + */ + String PROTOCOL_REDIS = "redis"; + /** + * protocol mongodb + */ + String PROTOCOL_MONGODB = "mongodb"; + /** + * protocol jmx + */ + String PROTOCOL_JMX = "jmx"; + /** + * protocol snmp + */ + String PROTOCOL_SNMP = "snmp"; + /** + * protocol ftp + */ + String PROTOCOL_FTP = "ftp"; + /** + * protocol ssl Certificate - custom + */ + String PROTOCOL_SSL_CERT = "ssl_cert"; + /** + * protocol rocketmq + */ + String PROTOCOL_ROCKETMQ = "rocketmq"; + /** + * protocol push + */ + String PROTOCOL_PUSH = "push"; + /** + * protocol prometheus + */ + String PROTOCOL_PROMETHEUS = "prometheus"; + /** + * protocol dns + */ + String PROTOCOL_DNS = "dns"; + /** + * protocol pop3 + */ + String PROTOCOL_POP3 = "pop3"; + /** + * protocol registry + */ + String PROTOCOL_REGISTRY = "registry"; + /** + * protocol http sd + */ + String PROTOCOL_HTTP_SD = "http_sd"; + /** + * protocol nacos sd + */ + String PROTOCOL_NACOS_SD = "nacos_sd"; + /** + * protocol dns sd + */ + String PROTOCOL_DNS_SD = "dns_sd"; + /** + * protocol eureka sd + */ + String PROTOCOL_EUREKA_SD = "eureka_sd"; + + /** + * protocol consul sd + */ + String PROTOCOL_CONSUL_SD = "consul_sd"; + /** + * protocol redfish + */ + String PROTOCOL_REDFISH = "redfish"; + /** + * protocol imap + */ + String PROTOCOL_IMAP = "imap"; + + /** + * protocol script + */ + String PROTOCOL_SCRIPT = "script"; + + /** + * protocol mqtt + */ + String PROTOCOL_MQTT = "mqtt"; + + /** + * protocol ipmi + */ + String PROTOCOL_IPMI = "ipmi"; + + // Protocol type related - end + + // http protocol related - start should reuse HttpHeaders as much as possible + /** + * verification method Bearer Token + */ + String BEARER_TOKEN = "Bearer Token"; + /** + * Bearer Token The authentication parameter character + */ + String BEARER = "Bearer"; + /** + * Basic auth parameter + */ + String BASIC = "Basic"; + /** + * verification method authentication method Basic Auth + */ + String BASIC_AUTH = "Basic Auth"; + /** + * verification method authentication method Digest Auth + */ + String DIGEST_AUTH = "Digest Auth"; + /** + * Analysis method resolution: default rule + */ + String PARSE_DEFAULT = "default"; + /** + * Analysis method resolution: json path + */ + String PARSE_JSON_PATH = "jsonPath"; + /** + * Analysis method resolution: custom xml path + */ + String PARSE_XML_PATH = "xmlPath"; + /** + * Analysis method Website availability monitoring rules Provide responseTime metrics + */ + String PARSE_WEBSITE = "website"; + /** + * Analysis method Sitemap site-wide availability monitoring rules + */ + String PARSE_SITE_MAP = "sitemap"; + /** + * Analysis method resolution: response header + */ + String PARSE_HEADER = "header"; + /** + * Parsing method prometheus exporter data + */ + String PARSE_PROMETHEUS = "prometheus"; + /** + * Parse response body as config/properties format + */ + String PARSE_CONFIG = "config"; + /** + * prometheus accept header + */ + String PARSE_PROMETHEUS_ACCEPT = "application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5,*/*;q=0.1"; + /** + * PromQL Prometheus Query Language + */ + String PARSE_PROM_QL = "PromQL"; + String PARSE_PROM_QL_VECTOR = "vector"; + String PARSE_PROM_QL_MATRIX = "matrix"; + + /** + * protocol kafka + */ + String PROTOCOL_KAFKA = "kclient"; + + /** + * protocol plc + */ + String PROTOCOL_PLC = "plc"; + + /** + * protocol modbus + */ + String PROTOCOL_MODBUS = "modbus"; + + /** + * protocol modbus + */ + String PROTOCOL_S7 = "s7"; +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/DispatchProperties.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/DispatchProperties.java new file mode 100644 index 0000000..a34677f --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/DispatchProperties.java @@ -0,0 +1,216 @@ +/* + * 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.collector.dispatch; + +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Schedule Distribution Task Configuration Properties + */ +@Component +@ConfigurationProperties(prefix = ConfigConstants.FunctionModuleConstants.COLLECTOR + + SignConstants.DOT + + ConfigConstants.FunctionModuleConstants.DISPATCH) +public class DispatchProperties { + + /** + * Scheduling entry configuration properties + */ + private EntranceProperties entrance; + + /** + * Schedule Data Export Configuration Properties + */ + private ExportProperties export; + + public EntranceProperties getEntrance() { + return entrance; + } + + public void setEntrance(EntranceProperties entrance) { + this.entrance = entrance; + } + + public ExportProperties getExport() { + return export; + } + + public void setExport(ExportProperties export) { + this.export = export; + } + + /** + * Scheduling entry configuration properties + * The entry can be netty information, http request, message middleware message request + */ + public static class EntranceProperties { + + /** + * netty server client config + */ + private NettyProperties netty; + + public NettyProperties getNetty() { + return netty; + } + + public void setNetty(NettyProperties netty) { + this.netty = netty; + } + + + /** + * Netty Properties + */ + public static class NettyProperties { + + /** + * whether netty scheduling is started + */ + private boolean enabled = false; + + /** + * this collector unique identity + * default is the host name + */ + private String identity; + + /** + * this collector mode + * public: for public network, support cluster + * private: for private network, support cloud-edge + */ + private String mode; + + /** + * connect cluster master host + */ + private String managerHost; + + /** + * connect cluster master port + */ + private int managerPort = 1158; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getIdentity() { + return identity; + } + + public void setIdentity(String identity) { + this.identity = identity; + } + + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + + public String getManagerHost() { + return managerHost; + } + + public void setManagerHost(String managerHost) { + this.managerHost = managerHost; + } + + public int getManagerPort() { + return managerPort; + } + + public void setManagerPort(int managerPort) { + this.managerPort = managerPort; + } + } + } + + /** + * Schedule Data Export Configuration Properties + */ + public static class ExportProperties { + + /** + * kafka configuration information + */ + private KafkaProperties kafka; + + public KafkaProperties getKafka() { + return kafka; + } + + public void setKafka(KafkaProperties kafka) { + this.kafka = kafka; + } + + /** + * Kafka Properties + */ + public static class KafkaProperties { + /** + * Whether the kafka data export is started + */ + private boolean enabled = true; + + /** + * kafka's connection server url + */ + private String servers = "http://127.0.0.1:2379"; + /** + * Topic name to send data to + */ + private String topic; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getServers() { + return servers; + } + + public void setServers(String servers) { + this.servers = servers; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsTaskDispatch.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsTaskDispatch.java new file mode 100644 index 0000000..1bb28b3 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/MetricsTaskDispatch.java @@ -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. + */ + +package org.apache.hertzbeat.collector.dispatch; + +import org.apache.hertzbeat.common.timer.Timeout; + +/** + * Metrics collection task scheduler interface + */ +public interface MetricsTaskDispatch { + + /** + * schedule task + * @param timeout timeout + */ + void dispatchMetricsTask(Timeout timeout); +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/WorkerPool.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/WorkerPool.java new file mode 100644 index 0000000..a42cc9a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/WorkerPool.java @@ -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.collector.dispatch; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.concurrent.ManagedExecutor; +import org.apache.hertzbeat.common.concurrent.ManagedExecutors; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Collection task worker thread pool + */ +@Component +@Slf4j +public class WorkerPool implements DisposableBean { + + private final ManagedExecutor workerExecutor; + + private final ManagedExecutor longRunningExecutor; + + public WorkerPool() { + this(VirtualThreadProperties.defaults()); + } + + @Autowired + public WorkerPool(VirtualThreadProperties virtualThreadProperties) { + VirtualThreadProperties properties = + virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties; + this.workerExecutor = createWorkerExecutor(properties); + this.longRunningExecutor = createLongRunningExecutor(properties, workerExecutor); + } + + private ManagedExecutor createWorkerExecutor(VirtualThreadProperties properties) { + Thread.UncaughtExceptionHandler handler = (thread, throwable) -> { + log.error("[Important] WorkerPool workerExecutor has uncaughtException.", throwable); + log.error("Thread Name {} : {}", thread.getName(), throwable.getMessage(), throwable); + }; + if (properties.enabled()) { + VirtualThreadProperties.PoolProperties poolProperties = properties.collector(); + return ManagedExecutors.newVirtualExecutor("collector-worker", "collect-worker-", + poolProperties.mode(), poolProperties.maxConcurrentJobs(), handler); + } + return ManagedExecutors.wrap("collector-worker", createLegacyExecutor(handler)); + } + + private ManagedExecutor createLongRunningExecutor(VirtualThreadProperties properties, ManagedExecutor fallback) { + if (!properties.enabled()) { + return fallback; + } + return ManagedExecutors.newPlatformExecutor("collector-long-running", "collect-long-running-", + (thread, throwable) -> { + log.error("[Important] WorkerPool longRunningExecutor has uncaughtException.", throwable); + log.error("Thread Name {} : {}", thread.getName(), throwable.getMessage(), throwable); + }); + } + + private ExecutorService createLegacyExecutor(Thread.UncaughtExceptionHandler handler) { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler(handler) + .setDaemon(true) + .setNameFormat("collect-worker-%d") + .build(); + int coreSize = Math.max(2, Runtime.getRuntime().availableProcessors()); + int maxSize = Runtime.getRuntime().availableProcessors() * 16; + return new ThreadPoolExecutor(coreSize, + maxSize, + 10, + TimeUnit.SECONDS, + new SynchronousQueue<>(), + threadFactory, + new ThreadPoolExecutor.AbortPolicy()); + } + + /** + * Run the collection task thread + * + * @param runnable Task + * @throws RejectedExecutionException when thread pool full + */ + public void executeJob(Runnable runnable) throws RejectedExecutionException { + workerExecutor.execute(runnable); + } + + /** + * Run the long-lived dispatcher job outside of the collector admission limit. + * + * @param runnable dispatcher task + */ + public void executeLongRunning(Runnable runnable) { + longRunningExecutor.execute(runnable); + } + + @Override + public void destroy() throws Exception { + workerExecutor.close(); + if (longRunningExecutor != workerExecutor) { + longRunningExecutor.close(); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/CollectServer.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/CollectServer.java new file mode 100644 index 0000000..a5bf494 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/CollectServer.java @@ -0,0 +1,311 @@ +/* + * 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.collector.dispatch.entrance; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.protobuf.ByteString; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.dispatch.CollectorInfoProperties; +import org.apache.hertzbeat.collector.dispatch.DispatchProperties; +import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService; +import org.apache.hertzbeat.collector.dispatch.entrance.processor.CollectCyclicDataProcessor; +import org.apache.hertzbeat.collector.dispatch.entrance.processor.CollectOneTimeDataProcessor; +import org.apache.hertzbeat.collector.dispatch.entrance.processor.DeleteCyclicTaskProcessor; +import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoCloseProcessor; +import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoOfflineProcessor; +import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoOnlineProcessor; +import org.apache.hertzbeat.collector.dispatch.entrance.processor.HeartbeatProcessor; +import org.apache.hertzbeat.collector.timer.TimerDispatch; +import org.apache.hertzbeat.common.concurrent.BackgroundTaskExecutor; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.common.entity.dto.CollectorInfo; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.remoting.RemotingClient; +import org.apache.hertzbeat.remoting.event.NettyEventListener; +import org.apache.hertzbeat.remoting.netty.NettyClientConfig; +import org.apache.hertzbeat.remoting.netty.NettyRemotingClient; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** + * collect server + */ +@Component +@Order +@ConditionalOnProperty(prefix = "collector.dispatch.entrance.netty", + name = "enabled", havingValue = "true") +@Slf4j +public class CollectServer implements CommandLineRunner { + + private final CollectJobService collectJobService; + + private final TimerDispatch timerDispatch; + + private final CollectorInfoProperties infoProperties; + + private RemotingClient remotingClient; + + private ScheduledExecutorService scheduledExecutor; + + private final ExecutorService heartbeatExecutor; + + private final Object heartbeatLock = new Object(); + + private boolean heartbeatRunning; + + private boolean heartbeatPending; + + private final Runnable closeApplicationAction; + + public CollectServer(final CollectJobService collectJobService, + final TimerDispatch timerDispatch, + final DispatchProperties properties, + final BackgroundTaskExecutor threadPool, + final CollectorInfoProperties infoProperties) { + this(collectJobService, timerDispatch, properties, threadPool, infoProperties, null, + VirtualThreadProperties.defaults()); + } + + public CollectServer(final CollectJobService collectJobService, + final TimerDispatch timerDispatch, + final DispatchProperties properties, + final BackgroundTaskExecutor threadPool, + final CollectorInfoProperties infoProperties, + final VirtualThreadProperties virtualThreadProperties) { + this(collectJobService, timerDispatch, properties, threadPool, infoProperties, null, virtualThreadProperties); + } + + @Autowired + public CollectServer(final CollectJobService collectJobService, + final TimerDispatch timerDispatch, + final DispatchProperties properties, + final BackgroundTaskExecutor threadPool, + final CollectorInfoProperties infoProperties, + final ConfigurableApplicationContext applicationContext, + final VirtualThreadProperties virtualThreadProperties) { + if (properties == null || properties.getEntrance() == null || properties.getEntrance().getNetty() == null) { + log.error("init error, please config dispatch entrance netty props in application.yml"); + throw new IllegalArgumentException("please config dispatch entrance netty props"); + } + DispatchProperties.EntranceProperties.NettyProperties nettyProperties = properties.getEntrance().getNetty(); + if (nettyProperties.getManagerHost() == null || nettyProperties.getManagerPort() == 0) { + throw new IllegalArgumentException("please config dispatch entrance netty master host and port"); + } + this.collectJobService = collectJobService; + this.timerDispatch = timerDispatch; + this.collectJobService.setCollectServer(this); + this.infoProperties = infoProperties; + this.heartbeatExecutor = createHeartbeatExecutor(virtualThreadProperties); + this.closeApplicationAction = createCloseApplicationAction(applicationContext); + this.init(properties, threadPool); + } + + private void init(final DispatchProperties properties, final BackgroundTaskExecutor threadPool) { + NettyClientConfig nettyClientConfig = new NettyClientConfig(); + DispatchProperties.EntranceProperties.NettyProperties nettyProperties = properties.getEntrance().getNetty(); + nettyClientConfig.setServerHost(nettyProperties.getManagerHost()); + nettyClientConfig.setServerPort(nettyProperties.getManagerPort()); + this.remotingClient = new NettyRemotingClient(nettyClientConfig, new CollectNettyEventListener(), threadPool); + + this.remotingClient.registerProcessor(ClusterMsg.MessageType.HEARTBEAT, new HeartbeatProcessor()); + this.remotingClient.registerProcessor(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK, new CollectCyclicDataProcessor(this)); + this.remotingClient.registerProcessor(ClusterMsg.MessageType.DELETE_CYCLIC_TASK, new DeleteCyclicTaskProcessor(this)); + this.remotingClient.registerProcessor(ClusterMsg.MessageType.ISSUE_ONE_TIME_TASK, new CollectOneTimeDataProcessor(this)); + this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_OFFLINE, new GoOfflineProcessor(timerDispatch)); + this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_ONLINE, new GoOnlineProcessor(timerDispatch)); + this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_CLOSE, + new GoCloseProcessor(this, timerDispatch, closeApplicationAction)); + } + + public void shutdown() { + if (this.scheduledExecutor != null) { + this.scheduledExecutor.shutdownNow(); + } + if (this.heartbeatExecutor != null) { + this.heartbeatExecutor.shutdownNow(); + } + + this.remotingClient.shutdown(); + } + + public CollectJobService getCollectJobService() { + return collectJobService; + } + + public void sendMsg(final ClusterMsg.Message message) { + this.remotingClient.sendMsg(message); + } + + void dispatchHeartbeat(String identity) { + if (heartbeatExecutor == null) { + sendHeartbeat(identity); + return; + } + synchronized (heartbeatLock) { + if (heartbeatRunning) { + heartbeatPending = true; + return; + } + heartbeatRunning = true; + } + submitHeartbeat(identity); + } + + @Override + public void run(String... args) throws Exception { + this.remotingClient.start(); + } + + /** + * CollectNettyEventListener + */ + public class CollectNettyEventListener implements NettyEventListener { + + @Override + public void onChannelActive(Channel channel) { + String identity = CollectServer.this.collectJobService.getCollectorIdentity(); + String mode = CollectServer.this.collectJobService.getCollectorMode(); + CollectorInfo collectorInfo = CollectorInfo.builder() + .name(identity) + .ip(infoProperties.getIp()) + .mode(mode) + .version(infoProperties.getVersion()) + // todo more info + .build(); + timerDispatch.goOnline(); + // send online message + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setIdentity(identity) + .setType(ClusterMsg.MessageType.GO_ONLINE) + .setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(collectorInfo))) + .build(); + CollectServer.this.sendMsg(message); + + if (scheduledExecutor == null) { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("HeartBeat Scheduler has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("heartbeat-worker-%d") + .build(); + scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory); + // schedule send heartbeat message + scheduledExecutor.scheduleAtFixedRate(() -> CollectServer.this.dispatchHeartbeat(identity), + 5, 5, TimeUnit.SECONDS); + } + } + + @Override + public void onChannelIdle(Channel channel) { + log.info("handle idle event triggered. collector is going offline."); + } + } + + private ExecutorService createHeartbeatExecutor(VirtualThreadProperties virtualThreadProperties) { + VirtualThreadProperties properties = + virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties; + if (!properties.enabled()) { + return null; + } + return Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name("heartbeat-worker-vt-", 0) + .uncaughtExceptionHandler((thread, throwable) -> { + log.error("HeartBeat worker has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .factory()); + } + + private void submitHeartbeat(String identity) { + boolean submitted = false; + try { + heartbeatExecutor.execute(() -> { + try { + sendHeartbeat(identity); + } finally { + onHeartbeatComplete(identity); + } + }); + submitted = true; + } finally { + if (!submitted) { + synchronized (heartbeatLock) { + heartbeatRunning = false; + heartbeatPending = false; + } + } + } + } + + private void onHeartbeatComplete(String identity) { + boolean shouldRunAgain; + synchronized (heartbeatLock) { + if (heartbeatPending) { + heartbeatPending = false; + shouldRunAgain = true; + } else { + heartbeatRunning = false; + shouldRunAgain = false; + } + } + if (shouldRunAgain) { + submitHeartbeat(identity); + } + } + + private void sendHeartbeat(String identity) { + try { + ClusterMsg.Message heartbeat = ClusterMsg.Message.newBuilder() + .setIdentity(identity) + .setDirection(ClusterMsg.Direction.REQUEST) + .setType(ClusterMsg.MessageType.HEARTBEAT) + .build(); + CollectServer.this.sendMsg(heartbeat); + log.info("collector send cluster server heartbeat, time: {}.", System.currentTimeMillis()); + } catch (Exception e) { + log.error("schedule send heartbeat to server error.{}", e.getMessage()); + } + } + + private Runnable createCloseApplicationAction(ConfigurableApplicationContext applicationContext) { + if (applicationContext == null) { + return () -> {}; + } + return () -> { + SpringApplication.exit(applicationContext, () -> 0); + if (applicationContext.isActive()) { + applicationContext.close(); + } + }; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/internal/CollectJobService.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/internal/CollectJobService.java new file mode 100644 index 0000000..616abdb --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/internal/CollectJobService.java @@ -0,0 +1,187 @@ +/* + * 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.collector.dispatch.entrance.internal; + +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.dispatch.DispatchProperties; +import org.apache.hertzbeat.collector.dispatch.WorkerPool; +import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer; +import org.apache.hertzbeat.collector.timer.TimerDispatch; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.ArrowUtil; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Collection job management provides api interface + */ +@Service +@Slf4j +public class CollectJobService { + + private static final String COLLECTOR_STR = "-collector"; + + private final TimerDispatch timerDispatch; + + private final WorkerPool workerPool; + + private final String collectorIdentity; + + private String mode = null; + + private CollectServer collectServer; + + public CollectJobService(TimerDispatch timerDispatch, DispatchProperties properties, WorkerPool workerPool) { + this.timerDispatch = timerDispatch; + this.workerPool = workerPool; + + Optional<DispatchProperties.EntranceProperties.NettyProperties> nettyPropertiesOptional = Optional.ofNullable(properties) + .map(DispatchProperties::getEntrance) + .map(DispatchProperties.EntranceProperties::getNetty) + .filter(DispatchProperties.EntranceProperties.NettyProperties::isEnabled); + + if (nettyPropertiesOptional.isEmpty()) { + collectorIdentity = CommonConstants.MAIN_COLLECTOR_NODE; + return; + } + + DispatchProperties.EntranceProperties.NettyProperties nettyProperties = nettyPropertiesOptional.get(); + mode = nettyProperties.getMode(); + if (StringUtils.hasText(nettyProperties.getIdentity())) { + collectorIdentity = nettyProperties.getIdentity(); + } else { + collectorIdentity = IpDomainUtil.getCurrentHostName() + COLLECTOR_STR; + log.info("user not config this collector identity, use [host name - host ip] default: {}.", collectorIdentity); + } + } + + /** + * Execute a one-time collection task and get the collected data response + * + * @param job Collect task details + * @return Collection results + */ + public List<CollectRep.MetricsData> collectSyncJobData(Job job) { + final List<CollectRep.MetricsData> metricsData = new LinkedList<>(); + final CountDownLatch countDownLatch = new CountDownLatch(1); + CollectResponseEventListener listener = new CollectResponseEventListener() { + @Override + public void response(List<CollectRep.MetricsData> responseMetrics) { + if (responseMetrics != null) { + metricsData.addAll(responseMetrics); + } + countDownLatch.countDown(); + } + }; + timerDispatch.addJob(job, listener); + try { + countDownLatch.await(120, TimeUnit.SECONDS); + } catch (Exception e) { + log.info("The sync task runs for 120 seconds with no response and returns"); + } + return metricsData; + } + + /** + * Execute a one-time collection task and send the collected data response + * + * @param oneTimeJob Collect task details + */ + public void collectSyncOneTimeJobData(Job oneTimeJob) { + workerPool.executeJob(() -> { + List<CollectRep.MetricsData> metricsDataList = this.collectSyncJobData(oneTimeJob); + byte[] msg = ArrowUtil.serializeMetricsData(metricsDataList); + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setMsg(ByteString.copyFrom(msg)) + .setDirection(ClusterMsg.Direction.REQUEST) + .setType(ClusterMsg.MessageType.RESPONSE_ONE_TIME_TASK_DATA) + .build(); + this.collectServer.sendMsg(message); + }); + } + + /** + * Issue periodic asynchronous collection tasks + * + * @param job Collect task details + */ + public void addAsyncCollectJob(Job job) { + timerDispatch.addJob(job.clone(), null); + } + + /** + * Cancel periodic asynchronous collection tasks + * + * @param jobId Job ID + */ + public void cancelAsyncCollectJob(Long jobId) { + if (jobId != null) { + timerDispatch.deleteJob(jobId, true); + } + } + + /** + * send async collect response data + * + * @param metricsData collect data + */ + public void sendAsyncCollectData(CollectRep.MetricsData metricsData) { + byte[] msg = ArrowUtil.serializeMetricsData(List.of(metricsData)); + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setIdentity(collectorIdentity) + .setMsg(ByteString.copyFrom(msg)) + .setDirection(ClusterMsg.Direction.REQUEST) + .setType(ClusterMsg.MessageType.RESPONSE_CYCLIC_TASK_DATA) + .build(); + this.collectServer.sendMsg(message); + } + + public void sendAsyncServiceDiscoveryData(CollectRep.MetricsData metricsData) { + byte[] msg = ArrowUtil.serializeMetricsData(List.of(metricsData)); + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setIdentity(collectorIdentity) + .setMsg(ByteString.copyFrom(msg)) + .setDirection(ClusterMsg.Direction.REQUEST) + .setType(ClusterMsg.MessageType.RESPONSE_CYCLIC_TASK_SD_DATA) + .build(); + this.collectServer.sendMsg(message); + } + + public String getCollectorIdentity() { + return collectorIdentity; + } + + public String getCollectorMode() { + return mode; + } + + public void setCollectServer(CollectServer collectServer) { + this.collectServer = collectServer; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/internal/CollectResponseEventListener.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/internal/CollectResponseEventListener.java new file mode 100644 index 0000000..54c13d4 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/internal/CollectResponseEventListener.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.dispatch.entrance.internal; + +import org.apache.hertzbeat.common.entity.message.CollectRep; + +import java.util.EventListener; +import java.util.List; + +/** + * One-time collection task response result listener + */ +public interface CollectResponseEventListener extends EventListener { + + /** + * Collection task completion result notification + * @param responseMetrics Response Metrics + */ + default void response(List<CollectRep.MetricsData> responseMetrics) {} +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/CollectCyclicDataProcessor.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/CollectCyclicDataProcessor.java new file mode 100644 index 0000000..f37f71d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/CollectCyclicDataProcessor.java @@ -0,0 +1,49 @@ +/* + * 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.collector.dispatch.entrance.processor; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle cyclic data message + */ +@Slf4j +public class CollectCyclicDataProcessor implements NettyRemotingProcessor { + private final CollectServer collectServer; + + public CollectCyclicDataProcessor(CollectServer collectServer) { + this.collectServer = collectServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + Job job = JsonUtil.fromJson(message.getMsg().toStringUtf8(), Job.class); + if (job == null) { + log.error("collector receive cyclic task job is null"); + return null; + } + collectServer.getCollectJobService().addAsyncCollectJob(job); + return null; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/CollectOneTimeDataProcessor.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/CollectOneTimeDataProcessor.java new file mode 100644 index 0000000..74f3d52 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/CollectOneTimeDataProcessor.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.dispatch.entrance.processor; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle one-time collect data response message + */ +public class CollectOneTimeDataProcessor implements NettyRemotingProcessor { + private final CollectServer collectServer; + + public CollectOneTimeDataProcessor(final CollectServer collectServer) { + this.collectServer = collectServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + Job oneTimeJob = JsonUtil.fromJson(message.getMsg().toStringUtf8(), Job.class); + collectServer.getCollectJobService().collectSyncOneTimeJobData(oneTimeJob); + return null; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/DeleteCyclicTaskProcessor.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/DeleteCyclicTaskProcessor.java new file mode 100644 index 0000000..bcf5dbd --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/DeleteCyclicTaskProcessor.java @@ -0,0 +1,54 @@ +/* + * 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.collector.dispatch.entrance.processor; + +import tools.jackson.core.type.TypeReference; +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +import java.util.List; + +/** + * handle delete cyclic task message + */ +@Slf4j +public class DeleteCyclicTaskProcessor implements NettyRemotingProcessor { + private final CollectServer collectServer; + + public DeleteCyclicTaskProcessor(CollectServer collectServer) { + this.collectServer = collectServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + TypeReference<List<Long>> typeReference = new TypeReference<>() {}; + List<Long> jobIds = JsonUtil.fromJson(message.getMsg().toStringUtf8(), typeReference); + if (jobIds == null || jobIds.isEmpty()) { + log.error("collector receive delete cyclic task job ids is null"); + return null; + } + for (Long jobId : jobIds) { + collectServer.getCollectJobService().cancelAsyncCollectJob(jobId); + } + return null; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/GoCloseProcessor.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/GoCloseProcessor.java new file mode 100644 index 0000000..b259af8 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/GoCloseProcessor.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.dispatch.entrance.processor; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer; +import org.apache.hertzbeat.collector.timer.TimerDispatch; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle collector close message + * Note: The collection task is closed and the connection to Manager is disconnected + */ +@Slf4j +public class GoCloseProcessor implements NettyRemotingProcessor { + private final CollectServer collectServer; + private final TimerDispatch timerDispatch; + private final Runnable closeApplicationAction; + + public GoCloseProcessor(final CollectServer collectServer, + final TimerDispatch timerDispatch, + final Runnable closeApplicationAction) { + this.collectServer = collectServer; + this.timerDispatch = timerDispatch; + this.closeApplicationAction = closeApplicationAction; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + if (message.getMsg().toStringUtf8().contains(CommonConstants.COLLECTOR_AUTH_FAILED)) { + log.error("[Auth Failed]receive client auth failed message and go close. {}", message.getMsg()); + } + this.timerDispatch.goOffline(); + this.collectServer.shutdown(); + closeApplicationAction.run(); + log.info("receive offline message and close success"); + return null; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/GoOfflineProcessor.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/GoOfflineProcessor.java new file mode 100644 index 0000000..ccacf94 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/GoOfflineProcessor.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.dispatch.entrance.processor; + +import com.google.protobuf.ByteString; +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.timer.TimerDispatch; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle collector offline message + * Note: The connection to Manager is not closed here, only the capture function is turned off + */ +@Slf4j +public class GoOfflineProcessor implements NettyRemotingProcessor { + + private final TimerDispatch timerDispatch; + + public GoOfflineProcessor(TimerDispatch timerDispatch) { + this.timerDispatch = timerDispatch; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + timerDispatch.goOffline(); + log.info("receive offline message and handle success"); + if (message.getMsg().toStringUtf8().contains(CommonConstants.COLLECTOR_AUTH_FAILED)) { + log.error("[Auth Failed]receive client auth failed message and go offline. {}", message.getMsg()); + return null; + } + return ClusterMsg.Message.newBuilder() + .setIdentity(message.getIdentity()) + .setDirection(ClusterMsg.Direction.RESPONSE) + .setMsg(ByteString.copyFromUtf8(String.valueOf(CommonConstants.SUCCESS_CODE))) + .build(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/GoOnlineProcessor.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/GoOnlineProcessor.java new file mode 100644 index 0000000..e35c66a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/GoOnlineProcessor.java @@ -0,0 +1,66 @@ +/* + * 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.collector.dispatch.entrance.processor; + +import com.google.protobuf.ByteString; +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.timer.TimerDispatch; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.ServerInfo; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.util.AesUtil; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle collector online message + * Note: This is not to re-open the connection with Manager, nor can it be done, just to re-open the collection function + */ +@Slf4j +public class GoOnlineProcessor implements NettyRemotingProcessor { + + private final TimerDispatch timerDispatch; + + public GoOnlineProcessor(TimerDispatch timerDispatch) { + this.timerDispatch = timerDispatch; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + if (message.getMsg().isEmpty()) { + log.warn("The message that server response to collector is empty, please upgrade server"); + } else { + ServerInfo serverInfo = JsonUtil.fromJson(message.getMsg().toStringUtf8(), ServerInfo.class); + if (serverInfo == null || serverInfo.getAesSecret() == null) { + log.warn("The message that server response to collector has not secret empty, please check"); + } else { + AesUtil.setDefaultSecretKey(serverInfo.getAesSecret()); + } + } + if (ClusterMsg.Direction.REQUEST.equals(message.getDirection())) { + timerDispatch.goOnline(); + } + log.info("receive online message and handle success"); + return ClusterMsg.Message.newBuilder() + .setIdentity(message.getIdentity()) + .setDirection(ClusterMsg.Direction.RESPONSE) + .setMsg(ByteString.copyFromUtf8(String.valueOf(CommonConstants.SUCCESS_CODE))) + .build(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/HeartbeatProcessor.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/HeartbeatProcessor.java new file mode 100644 index 0000000..9fc036c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/HeartbeatProcessor.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.dispatch.entrance.processor; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle heartbeat message + */ +@Slf4j +public class HeartbeatProcessor implements NettyRemotingProcessor { + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + log.info("collector receive manager server response heartbeat, time: {}. ", System.currentTimeMillis()); + return null; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/export/NettyDataQueue.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/export/NettyDataQueue.java new file mode 100644 index 0000000..bdd001f --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/export/NettyDataQueue.java @@ -0,0 +1,133 @@ +/* + * 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.collector.dispatch.export; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * for collector instance + * send collect response data by netty + */ +@Slf4j +@Configuration +@ConditionalOnProperty( + prefix = NettyDataQueue.NETTY_DATA_QUEUE_PREFIX, + name = NettyDataQueue.NAME, + havingValue = NettyDataQueue.NETTY +) +public class NettyDataQueue implements CommonDataQueue { + + /** + * netty data queue prefix. + */ + protected static final String NETTY_DATA_QUEUE_PREFIX = "common.queue"; + + /** + * name constants + */ + protected static final String NAME = "type"; + + /** + * havingValue constants + */ + protected static final String NETTY = "netty"; + + private final CollectJobService collectJobService; + + public NettyDataQueue(CollectJobService collectJobService) { + this.collectJobService = collectJobService; + } + + @Override + public CollectRep.MetricsData pollMetricsDataToAlerter() { + return null; + } + + @Override + public CollectRep.MetricsData pollMetricsDataToStorage() { + return null; + } + + @Override + public CollectRep.MetricsData pollServiceDiscoveryData() { + return null; + } + + @Override + public void sendMetricsData(CollectRep.MetricsData metricsData) { + collectJobService.sendAsyncCollectData(metricsData); + } + + @Override + public void sendMetricsDataToStorage(CollectRep.MetricsData metricsData) { + + } + + @Override + public void sendServiceDiscoveryData(CollectRep.MetricsData metricsData) { + collectJobService.sendAsyncServiceDiscoveryData(metricsData); + } + + @Override + public void sendLogEntry(LogEntry logEntry) { + + } + + @Override + public LogEntry pollLogEntry() throws InterruptedException { + return null; + } + + @Override + public void sendLogEntryToStorage(LogEntry logEntry) { + + } + + @Override + public LogEntry pollLogEntryToStorage() throws InterruptedException { + return null; + } + + @Override + public void sendLogEntryToAlertBatch(List<LogEntry> logEntries) { + + } + + @Override + public List<LogEntry> pollLogEntryToAlertBatch(int maxBatchSize) throws InterruptedException { + return List.of(); + } + + @Override + public void sendLogEntryToStorageBatch(List<LogEntry> logEntries) { + + } + + @Override + public List<LogEntry> pollLogEntryToStorageBatch(int maxBatchSize) throws InterruptedException { + return List.of(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/DataUnit.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/DataUnit.java new file mode 100644 index 0000000..8d1068d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/DataUnit.java @@ -0,0 +1,86 @@ +/* + * 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.collector.dispatch.unit; + +/** + * the enum of data size + */ +public enum DataUnit { + /** + * byte + */ + B("B", 1), + /** + * kilobyte + */ + KB("KB", 1024), + /** + * kilobyte + */ + K("K", 1024), + /** + * kilobyte + */ + KI("KI", 1024), + /** + * megabyte + * 1024 * 1024 + */ + MB("MB", 1_048_576), + /** + * megabyte + * 1024 * 1024 + */ + MI("MI", 1_048_576), + /** + * megabyte + * 1024 * 1024 + */ + M("M", 1_048_576), + /** + * gigabyte + * 1024 * 1024 * 1024 + */ + GB("GB", 1_073_741_824), + /** + * gigabyte + * 1024 * 1024 * 1024 + */ + GI("GI", 1_073_741_824), + /** + * gigabyte + * 1024 * 1024 * 1024 + */ + G("G", 1_073_741_824); + + private final String unit; + private final long scale; + + DataUnit(String unit, long scale) { + this.unit = unit; + this.scale = scale; + } + + public String getUnit() { + return unit; + } + + public long getScale() { + return scale; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/TimeLengthUnit.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/TimeLengthUnit.java new file mode 100644 index 0000000..b7313a9 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/TimeLengthUnit.java @@ -0,0 +1,68 @@ +/* + * 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.collector.dispatch.unit; + +/** + * the enum of time length + */ +public enum TimeLengthUnit { + /** + * NANOSECONDS + */ + NS("NS", 1), + /** + * MICROSECONDS + */ + US("US", 1000), + /** + * MILLISECONDS + */ + MS("MS", 1000_000), + /** + * SECONDS + */ + S("S", 1000_000_000), + /** + * MINUTES + */ + MIN("MIN", 60_000_000_000L), + /** + * HOURS + */ + H("H", 3600_000_000_000L), + /** + * DAYS + */ + D("D", 86_400_000_000_000L); + + private final String unit; + private final long scale; + + TimeLengthUnit(String unit, long scale) { + this.unit = unit; + this.scale = scale; + } + + public String getUnit() { + return unit; + } + + public long getScale() { + return scale; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/UnitConvert.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/UnitConvert.java new file mode 100644 index 0000000..fd50507 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/UnitConvert.java @@ -0,0 +1,45 @@ +/* + * 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.collector.dispatch.unit; + +/** + * the interface of unit convert + * Unit conversion interface classes and deal with mon. org.apache.hertzbeat.com entity. + * The job. The Metrics# units + */ +public interface UnitConvert { + + /** + * convert originUnit value to newUnit value + * + * @param value The collected value + * @param originUnit The unit to which the original value corresponds. + * @param newUnit The unit to display + * @return Converted value + */ + String convert(String value, String originUnit, String newUnit); + + /** + * check the unit and confirm to use this implement class + * Checks for units before and after xxx -> xxx, also to make sure that the implementation class is being used + * + * @param unit unit + * @return true/false + */ + boolean checkUnit(String unit); +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/impl/AbstractUnitConvert.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/impl/AbstractUnitConvert.java new file mode 100644 index 0000000..aef37c4 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/impl/AbstractUnitConvert.java @@ -0,0 +1,75 @@ +/* + * 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.collector.dispatch.unit.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.dispatch.unit.UnitConvert; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Map; + +/** + * abstract UnitConvert + */ +@Slf4j +public abstract class AbstractUnitConvert implements UnitConvert { + + + @Override + public String convert(String value, String originUnit, String newUnit) { + if (StringUtils.isBlank(value)) { + return null; + } + if (!checkUnit(originUnit) || !checkUnit(newUnit)) { + return null; + } + + if (originUnit.equalsIgnoreCase(newUnit)) { + log.warn("The origin unit is the same as the new unit, no need to convert"); + return value; + } + BigDecimal wrappedValue = new BigDecimal(value); + + Map<String, Long> unitMap = convertUnitEnumToMap(); + + //There is no need to check again,as both the originUnit and newUnit have already been checked for existence. + Long multipleScale = unitMap.get(originUnit.toUpperCase()); + wrappedValue = wrappedValue.multiply(new BigDecimal(multipleScale)); + + Long divideScale = unitMap.get(newUnit.toUpperCase()); + wrappedValue = wrappedValue.divide(new BigDecimal(divideScale), 12, RoundingMode.HALF_UP); + + return wrappedValue.setScale(4, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString(); + } + + @Override + public boolean checkUnit(String unit) { + if (StringUtils.isBlank(unit)) { + return false; + } + Map<String, Long> convertedUnitEnumToMap = convertUnitEnumToMap(); + return convertedUnitEnumToMap.containsKey(unit.toUpperCase()); + } + + /*** + * this method can be used to convert specific enum to map by the Inherited class + */ + abstract Map<String, Long> convertUnitEnumToMap(); +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/impl/DataSizeConvert.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/impl/DataSizeConvert.java new file mode 100644 index 0000000..5cc57a5 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/impl/DataSizeConvert.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.dispatch.unit.impl; + +import org.apache.hertzbeat.collector.dispatch.unit.DataUnit; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * the convert of data size + */ +@Component +public final class DataSizeConvert extends AbstractUnitConvert { + + + /** + * convert the enumeration of DataUnit to map where the key is the unit and the value is the conversion coefficient. + */ + @Override + Map<String, Long> convertUnitEnumToMap() { + return Arrays.stream(DataUnit.values()).collect(Collectors.toMap(DataUnit::getUnit, DataUnit::getScale)); + } + + +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/impl/TimeLengthConvert.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/impl/TimeLengthConvert.java new file mode 100644 index 0000000..5092d47 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/unit/impl/TimeLengthConvert.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.dispatch.unit.impl; + +import org.apache.hertzbeat.collector.dispatch.unit.TimeLengthUnit; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * the convert of time length + */ +@Component +public final class TimeLengthConvert extends AbstractUnitConvert { + + + /** + * convert the enumeration of TimeLengthUnit to map where the key is the unit and the value is the conversion coefficient. + */ + @Override + Map<String, Long> convertUnitEnumToMap() { + return Arrays.stream(TimeLengthUnit.values()).collect(Collectors.toMap(TimeLengthUnit::getUnit, TimeLengthUnit::getScale)); + } + + +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatch.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatch.java new file mode 100644 index 0000000..574812d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatch.java @@ -0,0 +1,77 @@ +/* + * 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.collector.timer; + +import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectResponseEventListener; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * timer dispatch service + */ +public interface TimerDispatch { + + /** + * Add new job + * + * @param addJob job + * @param eventListener One-time synchronous task listener, asynchronous task does not need listener + */ + void addJob(Job addJob, CollectResponseEventListener eventListener); + + /** + * Cyclic job + * @param timerTask timerTask + * @param interval collect interval + * @param timeUnit time unit + */ + void cyclicJob(WheelTimerTask timerTask, long interval, TimeUnit timeUnit); + + /** + * Cyclic job + * @param timerTask timerTask + */ + void cyclicJob(WheelTimerTask timerTask); + + /** + * Delete existing job + * @param jobId jobId + * @param isCyclic Whether it is a periodic task, true is, false is a temporary task + */ + void deleteJob(long jobId, boolean isCyclic); + + /** + * job dispatcher go online + */ + void goOnline(); + + /** + * job dispatcher go offline + */ + void goOffline(); + + /** + * response sync collect task data + * @param jobId jobId + * @param metricsDataTemps collect data + */ + void responseSyncJobData(long jobId, List<CollectRep.MetricsData> metricsDataTemps); +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatcher.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatcher.java new file mode 100644 index 0000000..797da52 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatcher.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.timer; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.constants.ScheduleTypeEnum; +import org.apache.hertzbeat.collector.dispatch.MetricsTaskDispatch; +import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectResponseEventListener; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.timer.HashedWheelTimer; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.common.timer.Timer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.scheduling.support.CronExpression; +import org.springframework.stereotype.Component; + +/** + * job timer dispatcher + */ +@Component +@Slf4j +public class TimerDispatcher implements TimerDispatch, DisposableBean { + + /** + * time round schedule + */ + private final Timer wheelTimer; + /** + * Existing periodic scheduled tasks + */ + private final Map<Long, Timeout> currentCyclicTaskMap; + /** + * Existing temporary scheduled tasks + */ + private final Map<Long, Timeout> currentTempTaskMap; + /** + * One-time task response listener holds + * jobId - listener + */ + private final Map<Long, CollectResponseEventListener> eventListeners; + + /** + * is dispatcher online running + */ + private final AtomicBoolean started; + + private final Supplier<MetricsTaskDispatch> metricsTaskDispatchSupplier; + + public TimerDispatcher() { + this(() -> timeout -> { + }); + } + + @Autowired + public TimerDispatcher(ObjectProvider<MetricsTaskDispatch> metricsTaskDispatchProvider) { + this(resolveMetricsTaskDispatchSupplier(metricsTaskDispatchProvider)); + } + + private TimerDispatcher(Supplier<MetricsTaskDispatch> metricsTaskDispatchSupplier) { + this.wheelTimer = new HashedWheelTimer(r -> { + Thread ret = new Thread(r, "wheelTimer"); + ret.setDaemon(true); + return ret; + }, 1, TimeUnit.SECONDS, 512); + this.currentCyclicTaskMap = new ConcurrentHashMap<>(8); + this.currentTempTaskMap = new ConcurrentHashMap<>(8); + this.eventListeners = new ConcurrentHashMap<>(8); + this.started = new AtomicBoolean(true); + this.metricsTaskDispatchSupplier = metricsTaskDispatchSupplier; + } + + private static Supplier<MetricsTaskDispatch> resolveMetricsTaskDispatchSupplier( + ObjectProvider<MetricsTaskDispatch> metricsTaskDispatchProvider) { + if (metricsTaskDispatchProvider == null) { + return () -> timeout -> { + }; + } + return metricsTaskDispatchProvider::getObject; + } + + @Override + public void addJob(Job addJob, CollectResponseEventListener eventListener) { + if (!this.started.get()) { + log.warn("Collector is offline, can not dispatch collect jobs."); + return; + } + // Delay dispatcher lookup to avoid a startup cycle with CommonDispatcher. + WheelTimerTask timerJob = new WheelTimerTask(addJob, metricsTaskDispatchSupplier); + if (addJob.isCyclic()) { + Long nextExecutionTime = getNextExecutionInterval(addJob); + Timeout timeout = wheelTimer.newTimeout(timerJob, nextExecutionTime, TimeUnit.SECONDS); + cancelPreviousTimeout(currentCyclicTaskMap.put(addJob.getId(), timeout)); + } else { + for (Metrics metric : addJob.getMetrics()) { + metric.setInterval(0L); + } + addJob.setIntervals(new ConcurrentLinkedDeque<>(List.of(0L))); + Timeout timeout = wheelTimer.newTimeout(timerJob, addJob.getInterval(), TimeUnit.SECONDS); + cancelPreviousTimeout(currentTempTaskMap.put(addJob.getId(), timeout)); + eventListeners.put(addJob.getId(), eventListener); + } + } + + @Override + public void cyclicJob(WheelTimerTask timerTask, long interval, TimeUnit timeUnit) { + if (!this.started.get()) { + log.warn("Collector is offline, can not dispatch collect jobs."); + return; + } + Long jobId = timerTask.getJob().getId(); + // whether is the job has been canceled + if (currentCyclicTaskMap.containsKey(jobId)) { + Timeout timeout = wheelTimer.newTimeout(timerTask, interval, TimeUnit.SECONDS); + cancelPreviousTimeout(currentCyclicTaskMap.put(timerTask.getJob().getId(), timeout)); + } + } + + @Override + public void cyclicJob(WheelTimerTask timerTask) { + Job job = timerTask.getJob(); + Long nextExecutionTime = getNextExecutionInterval(job); + cyclicJob(timerTask, nextExecutionTime, TimeUnit.SECONDS); + } + + @Override + public void deleteJob(long jobId, boolean isCyclic) { + if (isCyclic) { + Timeout timeout = currentCyclicTaskMap.remove(jobId); + if (timeout != null) { + timeout.cancel(); + } + } else { + Timeout timeout = currentTempTaskMap.remove(jobId); + if (timeout != null) { + timeout.cancel(); + } + } + } + + @Override + public void goOnline() { + currentCyclicTaskMap.forEach((key, value) -> value.cancel()); + currentCyclicTaskMap.clear(); + currentTempTaskMap.forEach((key, value) -> value.cancel()); + currentTempTaskMap.clear(); + started.set(true); + } + + @Override + public void goOffline() { + started.set(false); + currentCyclicTaskMap.forEach((key, value) -> value.cancel()); + currentCyclicTaskMap.clear(); + currentTempTaskMap.forEach((key, value) -> value.cancel()); + currentTempTaskMap.clear(); + } + + + @Override + public void responseSyncJobData(long jobId, List<CollectRep.MetricsData> metricsDataTemps) { + currentTempTaskMap.remove(jobId); + CollectResponseEventListener eventListener = eventListeners.remove(jobId); + if (eventListener != null) { + eventListener.response(metricsDataTemps); + } + } + + @Override + public void destroy() throws Exception { + this.wheelTimer.stop(); + } + + private void cancelPreviousTimeout(Timeout previousTimeout) { + if (previousTimeout != null) { + previousTimeout.cancel(); + } + } + + public Long getNextExecutionInterval(Job job) { + if (ScheduleTypeEnum.CRON.getType().equals(job.getScheduleType()) && job.getCronExpression() != null && !job.getCronExpression().isEmpty()) { + try { + CronExpression cronExpression = CronExpression.parse(job.getCronExpression()); + ZonedDateTime nextExecutionTime = cronExpression.next(ZonedDateTime.now()); + long delay = Duration.between(ZonedDateTime.now(), nextExecutionTime).toMillis(); + // Convert to seconds and ensure non-negative + return Math.max(0, delay / 1000); + } catch (Exception e) { + log.error("Invalid cron expression: {}", job.getCronExpression(), e); + // Fall back to interval scheduling if cron is invalid + return job.getInterval(); + } + } else { + if (job.getDispatchTime() > 0) { + long spendTime = System.currentTimeMillis() - job.getDispatchTime(); + // Calculate remaining interval in seconds, preserving millisecond precision + long intervalMs = job.getInterval() * 1000 - spendTime; + // Ensure non-negative + return Math.max(0, intervalMs / 1000); + } + return job.getInterval(); + } + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/WheelTimerTask.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/WheelTimerTask.java new file mode 100644 index 0000000..30d97ba --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/WheelTimerTask.java @@ -0,0 +1,106 @@ +/* + * 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.collector.timer; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.dispatch.MetricsTaskDispatch; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.common.timer.TimerTask; +import org.apache.hertzbeat.common.util.AesUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Timer Task implementation + */ +@Slf4j +public class WheelTimerTask implements TimerTask { + + private final Job job; + private final Supplier<MetricsTaskDispatch> metricsTaskDispatchSupplier; + private static final Gson GSON = new Gson(); + + public WheelTimerTask(Job job, MetricsTaskDispatch metricsTaskDispatch) { + this(job, () -> metricsTaskDispatch); + } + + public WheelTimerTask(Job job, Supplier<MetricsTaskDispatch> metricsTaskDispatchSupplier) { + this.metricsTaskDispatchSupplier = metricsTaskDispatchSupplier; + this.job = job; + // The initialization job will monitor the actual parameter value and replace the collection field + initJobMetrics(job); + } + + /** + * Initialize job fill information + * @param job job + */ + private void initJobMetrics(Job job) { + List<Configmap> config = job.getConfigmap(); + Map<String, Configmap> configmap = config.stream() + .peek(item -> { + // decode password + if (item.getType() == CommonConstants.PARAM_TYPE_PASSWORD && item.getValue() != null) { + String decodeValue = AesUtil.aesDecode(String.valueOf(item.getValue())); + if (decodeValue == null) { + log.error("Aes Decode value {} error.", item.getValue()); + } + item.setValue(decodeValue); + } else if (item.getValue() != null && item.getValue() instanceof String value) { + item.setValue(value.trim()); + } + }) + .collect(Collectors.toMap(Configmap::getKey, item -> item, (key1, key2) -> key1)); + List<Metrics> metrics = job.getMetrics(); + List<Metrics> metricsTmp = new ArrayList<>(metrics.size()); + for (Metrics metric : metrics) { + JsonElement jsonElement = GSON.toJsonTree(metric); + CollectUtil.replaceSmilingPlaceholder(jsonElement, configmap); + metric = GSON.fromJson(jsonElement, Metrics.class); + if (job.getApp().equals(DispatchConstants.PROTOCOL_PUSH)) { + CollectUtil.replaceFieldsForPushStyleMonitor(metric, configmap); + } + metricsTmp.add(metric); + } + job.setMetrics(metricsTmp); + job.initIntervals(); + } + + + @Override + public void run(Timeout timeout) throws Exception { + job.setDispatchTime(System.currentTimeMillis()); + metricsTaskDispatchSupplier.get().dispatchMetricsTask(timeout); + } + + public Job getJob() { + return job; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/CollectUtil.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/CollectUtil.java new file mode 100644 index 0000000..7d3bcab --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/CollectUtil.java @@ -0,0 +1,513 @@ +/* + * 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.collector.util; + +import com.beetstra.jutf7.CharsetProvider; +import tools.jackson.core.type.TypeReference; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.JsonUtil; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * util for collector + */ +@Slf4j +public final class CollectUtil { + + private static final int DEFAULT_TIMEOUT = 60000; + private static final int HEX_STR_WIDTH = 2; + private static final String SMILING_PLACEHOLDER = "^_^"; + private static final String SMILING_PLACEHOLDER_REX = "\\^_\\^"; + private static final String SMILING_PLACEHOLDER_REGEX = "(\\^_\\^)(\\w|-|$|\\.)+(\\^_\\^)"; + private static final Pattern SMILING_PLACEHOLDER_REGEX_PATTERN = Pattern.compile(SMILING_PLACEHOLDER_REGEX); + private static final String CRYING_PLACEHOLDER_REX = "\\^o\\^"; + private static final String CRYING_PLACEHOLDER_REGEX = "(\\^o\\^)(\\w|-|$|\\.)+(\\^o\\^)"; + private static final Pattern CRYING_PLACEHOLDER_REGEX_PATTERN = Pattern.compile(CRYING_PLACEHOLDER_REGEX); + private static final List<String> UNIT_SYMBOLS = Arrays.asList("%", "G", "g", "M", "m", "K", "k", "B", "b", "Ki", "Mi", "Gi"); + private static final Gson GSON = new Gson(); + /** + * Regularly verifying whether a string is a combination of numbers and units + */ + private static final String DOUBLE_AND_UNIT_CHECK_REGEX = "^[.\\d+" + String.join("", UNIT_SYMBOLS) + "]+$"; + + /** + * private constructor, not allow to create instance. + */ + private CollectUtil() { + } + + /** + * count match keyword number + * + * @param content content + * @param keyword keyword + * @return match num + */ + public static int countMatchKeyword(String content, String keyword) { + if (StringUtils.isAnyEmpty(content, keyword)) { + return 0; + } + try { + Pattern pattern = Pattern.compile(keyword); + Matcher matcher = pattern.matcher(content); + int count = 0; + while (matcher.find()) { + count++; + } + return count; + } catch (Exception e) { + return 0; + } + } + + public static DoubleAndUnit extractDoubleAndUnitFromStr(String str) { + if (StringUtils.isEmpty(str)) { + return null; + } + DoubleAndUnit doubleAndUnit = new DoubleAndUnit(); + try { + Double doubleValue = Double.parseDouble(str); + doubleAndUnit.setValue(doubleValue); + return doubleAndUnit; + } catch (Exception e) { + log.debug(e.getMessage()); + } + + if (!str.matches(DOUBLE_AND_UNIT_CHECK_REGEX)) { + return doubleAndUnit; + } + // extract unit from str value, eg: 23.43GB, 33KB, 44.22G + try { + // B KB MB GB % .... + for (String unitSymbol : UNIT_SYMBOLS) { + int index = str.indexOf(unitSymbol); + if (index == 0) { + Double doubleValue = 0d; + String unit = str.trim(); + doubleAndUnit.setValue(doubleValue); + doubleAndUnit.setUnit(unit); + return doubleAndUnit; + } + if (index > 0) { + Double doubleValue = Double.parseDouble(str.substring(0, index)); + String unit = str.substring(index).trim(); + doubleAndUnit.setValue(doubleValue); + doubleAndUnit.setUnit(unit); + return doubleAndUnit; + } + } + } catch (Exception e) { + log.debug(e.getMessage()); + } + return null; + } + + /** + * get timeout integer + * + * @param timeout timeout str + * @return timeout + */ + public static int getTimeout(String timeout) { + return getTimeout(timeout, DEFAULT_TIMEOUT); + } + + /** + * get timeout integer or default value + * + * @param timeout timeout str + * @param defaultTimeout default timeout + * @return timeout + */ + public static int getTimeout(String timeout, int defaultTimeout) { + if (StringUtils.isEmpty(timeout)) { + return defaultTimeout; + } + try { + return Double.valueOf(timeout).intValue(); + } catch (Exception e) { + return defaultTimeout; + } + } + + /** + * assert prom field + */ + public static Boolean assertPromRequireField(String aliasField) { + return CommonConstants.PROM_TIME.equals(aliasField) + || CommonConstants.PROM_VALUE.equals(aliasField) + || CommonConstants.PROM_METRIC_VALUE.equals(aliasField); + } + + /** + * Check if "value" field should be treated as a label instead of special field + */ + public static Boolean isValueAsLabel(String aliasField, JsonObject metric) { + return CommonConstants.PROM_VALUE.equals(aliasField) && null != metric && metric.has(aliasField); + } + + /** + * is contains cryPlaceholder ^o^xxx^o^ + * + * @param jsonElement json element + * @return return true when contains + */ + public static boolean containCryPlaceholder(JsonElement jsonElement) { + String jsonStr = jsonElement.toString(); + return CRYING_PLACEHOLDER_REGEX_PATTERN.matcher(jsonStr).find(); + } + + public static boolean notContainCryPlaceholder(JsonElement jsonElement) { + return !containCryPlaceholder(jsonElement); + } + + /** + * match existed cry placeholder fields ^o^field^o^ + * + * @param jsonElement json element + * @return match field str + */ + public static Set<String> matchCryPlaceholderField(JsonElement jsonElement) { + String jsonStr = jsonElement.toString(); + return CRYING_PLACEHOLDER_REGEX_PATTERN.matcher(jsonStr).results() + .map(item -> item.group().replaceAll(CRYING_PLACEHOLDER_REX, "")) + .collect(Collectors.toSet()); + } + + private static String replaceSpecialCharacterIfNeeded(String value, + String replacePattern, Matcher matcher, + Map<String, Configmap> configmap) { + matcher.reset(); + while (matcher.find()) { + String group = matcher.group(); + String replaceField = group.replaceAll(replacePattern, ""); + Configmap param = configmap.get(replaceField); + if (param == null) { + continue; + } + if (param.getValue() != null) { + value = value.replace(group, (String) param.getValue()); + } else if (group.length() == value.length()) { + value = null; + break; + } else { + value = value.replace(group, ""); + } + } + return value; + } + + /** + * replace cry placeholder to metrics + * + * @param metricItem metric item + * @param configmap configmap + * @return metrics + */ + public static Metrics replaceCryPlaceholderToMetrics(Metrics metricItem, Map<String, Configmap> configmap) { + JsonElement metricJson = GSON.toJsonTree(metricItem); + CollectUtil.replaceCryPlaceholder(metricJson, configmap); + return GSON.fromJson(metricJson, Metrics.class); + } + + /** + * json parameter replacement + * + * @param jsonElement json + * @param configmap parameter map + * @return json + */ + public static JsonElement replaceCryPlaceholder(JsonElement jsonElement, Map<String, Configmap> configmap) { + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + Iterator<Map.Entry<String, JsonElement>> iterator = jsonObject.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry<String, JsonElement> entry = iterator.next(); + JsonElement element = entry.getValue(); + // Replace normal VALUE value + if (!element.isJsonPrimitive()) { + jsonObject.add(entry.getKey(), replaceCryPlaceholder(entry.getValue(), configmap)); + continue; + } + // Replace normal VALUE value + // Check if there are special characters Replace + String value = element.getAsString(); + Matcher cryingMatcher = CRYING_PLACEHOLDER_REGEX_PATTERN.matcher(value); + if (!cryingMatcher.find()) { + continue; + } + // Replace special characters + value = replaceSpecialCharacterIfNeeded(value, CRYING_PLACEHOLDER_REX, cryingMatcher, configmap); + jsonObject.addProperty(entry.getKey(), value); + } + } else if (jsonElement.isJsonArray()) { + JsonArray jsonArray = jsonElement.getAsJsonArray(); + Iterator<JsonElement> iterator = jsonArray.iterator(); + int index = 0; + while (iterator.hasNext()) { + JsonElement element = iterator.next(); + + if (!element.isJsonPrimitive()) { + jsonArray.set(index, replaceCryPlaceholder(element, configmap)); + index++; + continue; + } + // Check if there are special characters Replace + String value = element.getAsString(); + Matcher cryingMatcher = CRYING_PLACEHOLDER_REGEX_PATTERN.matcher(value); + if (!cryingMatcher.find()) { + index++; + continue; + } + // Replace special characters + value = replaceSpecialCharacterIfNeeded(value, CRYING_PLACEHOLDER_REX, cryingMatcher, configmap); + jsonArray.set(index, value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)); + index++; + } + } + return jsonElement; + } + + /** + * json parameter replacement + * + * @param jsonElement json + * @param configmap parameter map + * @return json + */ + public static JsonElement replaceSmilingPlaceholder(JsonElement jsonElement, Map<String, Configmap> configmap) { + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + Iterator<Map.Entry<String, JsonElement>> iterator = jsonObject.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry<String, JsonElement> entry = iterator.next(); + JsonElement element = entry.getValue(); + String key = entry.getKey(); + // Replace the attributes of the KEY-VALUE case such as http headers params + if (key != null && key.startsWith(SMILING_PLACEHOLDER) && key.endsWith(SMILING_PLACEHOLDER)) { + key = key.replaceAll(SMILING_PLACEHOLDER_REX, ""); + Configmap param = configmap.get(key); + if (param != null && param.getType() == CommonConstants.PARAM_TYPE_MAP) { + String jsonValue = (String) param.getValue(); + TypeReference<Map<String, String>> typeReference = new TypeReference<>() { + }; + Map<String, String> map = JsonUtil.fromJson(jsonValue, typeReference); + if (map != null) { + map.forEach((name, value) -> { + if (!StringUtils.isEmpty(name)) { + jsonObject.addProperty(name, value); + } + }); + } + } + iterator.remove(); + continue; + } + // Replace normal VALUE value + if (!element.isJsonPrimitive()) { + jsonObject.add(entry.getKey(), replaceSmilingPlaceholder(entry.getValue(), configmap)); + continue; + } + // Check if there are special characters Replace + String value = element.getAsString(); + Matcher smilingMatcher = SMILING_PLACEHOLDER_REGEX_PATTERN.matcher(value); + if (!smilingMatcher.find()) { + continue; + } + // Replace special characters + value = replaceSpecialCharacterIfNeeded(value, SMILING_PLACEHOLDER_REX, smilingMatcher, configmap); + jsonObject.addProperty(entry.getKey(), value); + } + } else if (jsonElement.isJsonArray()) { + JsonArray jsonArray = jsonElement.getAsJsonArray(); + int index = 0; + while (index < jsonArray.size()) { + JsonElement element = jsonArray.get(index); + if (element.isJsonPrimitive()) { + // Check if there are special characters Replace + String value = element.getAsString(); + Matcher smilingMatcher = SMILING_PLACEHOLDER_REGEX_PATTERN.matcher(value); + if (smilingMatcher.find()) { + smilingMatcher.reset(); + String[] arrayValues = null; + while (smilingMatcher.find()) { + String group = smilingMatcher.group(); + String replaceField = group.replaceAll(SMILING_PLACEHOLDER_REX, ""); + Configmap param = configmap.get(replaceField); + if (param != null) { + if (param.getValue() == null) { + if (group.length() == value.length()) { + value = null; + break; + } else { + value = value.replace(group, ""); + } + } else if (param.getType() == CommonConstants.PARAM_TYPE_ARRAY) { + arrayValues = String.valueOf(param.getValue()).split(","); + } else { + value = value.replace(group, (String) param.getValue()); + } + } else { + value = null; + break; + } + } + if (arrayValues != null) { + jsonArray.remove(index); + index--; + for (String arrayValue : arrayValues) { + jsonArray.add(arrayValue); + index++; + } + } else { + jsonArray.set(index, value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)); + } + } + } else { + jsonArray.set(index, replaceSmilingPlaceholder(element, configmap)); + } + index++; + } + } + return jsonElement; + } + + public static String replaceUriSpecialChar(String uri) { + uri = uri.replaceAll(" ", "%20"); + // todo more special + return uri; + } + + public static List<Map<String, Configmap>> getConfigmapFromPreCollectData(CollectRep.MetricsData metricsData) { + if (metricsData.getValuesCount() <= 0 || metricsData.getFieldsCount() <= 0) { + return new LinkedList<>(); + } + List<Map<String, Configmap>> mapList = new LinkedList<>(); + for (CollectRep.ValueRow valueRow : metricsData.getValues()) { + if (valueRow.getColumnsCount() != metricsData.getFieldsCount()) { + continue; + } + Map<String, Configmap> configmapMap = new HashMap<>(valueRow.getColumnsCount()); + int index = 0; + for (CollectRep.Field field : metricsData.getFields()) { + String value = valueRow.getColumns(index); + index++; + Configmap configmap = new Configmap(field.getName(), value, Integer.valueOf(field.getType()).byteValue()); + configmapMap.put(field.getName(), configmap); + } + mapList.add(configmapMap); + } + return mapList; + } + + public static void replaceFieldsForPushStyleMonitor(Metrics metrics, Map<String, Configmap> configmap) { + + List<Metrics.Field> pushFieldList = JsonUtil.fromJson((String) configmap.get("fields").getValue(), new TypeReference<>() { + }); + metrics.setFields(pushFieldList); + } + + /** + * convert 16 hexString to byte[] + * eg: 302c0201010409636f6d6d756e697479a11c020419e502e7020100020100300e300c06082b060102010102000500 + * Hexadecimal strings are case-insensitive, and the returned arrays are the same + * @param hexString 16 hexString + * @return byte[] + */ + public static byte[] fromHexString(String hexString) { + if (StringUtils.isEmpty(hexString)) { + return null; + } + byte[] bytes = new byte[hexString.length() / HEX_STR_WIDTH]; + String hex; + for (int i = 0; i < hexString.length() / HEX_STR_WIDTH; i++) { + hex = hexString.substring(i * HEX_STR_WIDTH, i * HEX_STR_WIDTH + HEX_STR_WIDTH); + bytes[i] = (byte) Integer.parseInt(hex, 16); + } + return bytes; + } + + /** + * convert original string to UTF-7 String + * + * @param original original text + * @param charset encode charset + * @return String + */ + public static String stringEncodeUtf7String(String original, String charset) { + return new String(original.getBytes(new CharsetProvider().charsetForName(charset)), StandardCharsets.US_ASCII); + } + + /** + * convert UTF-7 string to original String + * + * @param encoded encoded String + * @param charset encode charset + * @return String + */ + public static String utf7StringDecodeString(String encoded, String charset) { + return new String(encoded.getBytes(StandardCharsets.US_ASCII), new CharsetProvider().charsetForName(charset)); + } + + /** + * double and unit + */ + public static final class DoubleAndUnit { + + private Double value; + private String unit; + + public Double getValue() { + return value; + } + + public void setValue(Double value) { + this.value = value; + } + + public String getUnit() { + return unit; + } + + public void setUnit(String unit) { + this.unit = unit; + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/JsonPathParser.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/JsonPathParser.java new file mode 100644 index 0000000..5deb848 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/JsonPathParser.java @@ -0,0 +1,76 @@ +/* + * 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.collector.util; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.Option; +import com.jayway.jsonpath.ParseContext; +import com.jayway.jsonpath.TypeRef; +import com.jayway.jsonpath.spi.cache.CacheProvider; +import com.jayway.jsonpath.spi.cache.LRUCache; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collections; +import java.util.List; + +/** + * json path parser + */ +public final class JsonPathParser { + + private static final ParseContext PARSER; + + static { + Configuration conf = Configuration.defaultConfiguration() + .addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL) + .addOptions(Option.ALWAYS_RETURN_LIST); + CacheProvider.setCache(new LRUCache(128)); + PARSER = JsonPath.using(conf); + } + + private JsonPathParser() { + } + + /** + * use json path to parse content + * @param content json content + * @param jsonPath jsonPath + * @return content [{'name': 'tom', 'speed': '433'},{'name': 'lili', 'speed': '543'}] + */ + public static List<Object> parseContentWithJsonPath(String content, String jsonPath) { + if (StringUtils.isAnyEmpty(content, jsonPath)) { + return Collections.emptyList(); + } + return PARSER.parse(content).read(jsonPath); + } + + /** + * use json path to parse content + * @param content json content + * @param jsonPath jsonPath + * @return content [{'name': 'tom', 'speed': '433'},{'name': 'lili', 'speed': '543'}] + */ + public static <T> T parseContentWithJsonPath(String content, String jsonPath, TypeRef<T> typeRef) { + if (StringUtils.isAnyEmpty(content, jsonPath)) { + return null; + } + return PARSER.parse(content).read(jsonPath, typeRef); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/PrivateKeyUtils.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/PrivateKeyUtils.java new file mode 100644 index 0000000..641eab2 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/PrivateKeyUtils.java @@ -0,0 +1,62 @@ +/* + * 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.collector.util; + +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Objects; + +/** + * private key util + * write private key to ~/.ssh + */ +@Slf4j +@UtilityClass +public class PrivateKeyUtils { + + private static final String KEY_PATH = System.getProperty("user.home") + "/.ssh"; + + /** + * write private key to ~/.ssh, filename is ~/.ssh/id_rsa_${host} + * @param host host + * @param keyStr key string + * @return key file path + * @throws IOException if ~/.ssh is not exist and create dir error + */ + public static String writePrivateKey(String host, String keyStr) throws IOException { + var sshPath = Paths.get(KEY_PATH); + if (!Files.exists(sshPath)) { + Files.createDirectories(sshPath); + } + var keyPath = Paths.get(KEY_PATH, "id_rsa_" + host); + if (!Files.exists(keyPath)) { + Files.writeString(keyPath, keyStr); + } else { + var oldKey = Files.readString(keyPath); + if (!Objects.equals(oldKey, keyStr)) { + Files.writeString(keyPath, keyStr); + } + } + return keyPath.toString(); + } + +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/TimeExpressionUtil.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/TimeExpressionUtil.java new file mode 100644 index 0000000..554cd97 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/util/TimeExpressionUtil.java @@ -0,0 +1,129 @@ +/* + * 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.collector.util; + +import org.apache.commons.lang3.StringUtils; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * time expression deal util + */ +public final class TimeExpressionUtil { + + private TimeExpressionUtil() { + } + + + /** + * localDatetime formatter + */ + private static final Map<String, Function<LocalDateTime, String>> FORMATTER = new LinkedHashMap<>(); + /** + * unit string mapping to TemporalUnit + */ + private static final Map<Character, TemporalUnit> UNIT_MAP = new HashMap<>(); + + private static final String FORMATTER_NAMES; + private static final String UNIT_NAMES; + + + static { + FORMATTER.put("now", date -> DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(date)); + FORMATTER.put("date", date -> DateTimeFormatter.ofPattern("yyyy-MM-dd").format(date)); + FORMATTER.put("timestamp10", date -> String.valueOf(date.toEpochSecond(ZoneOffset.UTC))); + FORMATTER.put("timestamp", date -> String.valueOf(date.toInstant(ZoneOffset.UTC).toEpochMilli())); + FORMATTER.put("time", date -> DateTimeFormatter.ofPattern("HH:mm:ss").format(date)); + FORMATTER.put("year", date -> DateTimeFormatter.ofPattern("yyyy").format(date)); + FORMATTER.put("month", date -> DateTimeFormatter.ofPattern("MM").format(date)); + FORMATTER.put("day", date -> DateTimeFormatter.ofPattern("dd").format(date)); + FORMATTER.put("hour", date -> DateTimeFormatter.ofPattern("HH").format(date)); + FORMATTER.put("minute", date -> DateTimeFormatter.ofPattern("mm").format(date)); + FORMATTER.put("millisecond", date -> DateTimeFormatter.ofPattern("SSS").format(date)); + FORMATTER.put("second", date -> DateTimeFormatter.ofPattern("ss").format(date)); + + UNIT_MAP.put('y', ChronoUnit.YEARS); + UNIT_MAP.put('M', ChronoUnit.MONTHS); + UNIT_MAP.put('d', ChronoUnit.DAYS); + UNIT_MAP.put('H', ChronoUnit.HOURS); + UNIT_MAP.put('m', ChronoUnit.MINUTES); + UNIT_MAP.put('s', ChronoUnit.SECONDS); + UNIT_MAP.put('w', ChronoUnit.WEEKS); + + FORMATTER_NAMES = String.join("|", FORMATTER.keySet()); + UNIT_NAMES = UNIT_MAP.keySet().stream().map(String::valueOf).collect(Collectors.joining("")); + } + + + public static String calculate(String template) { + return calculate(template, LocalDateTime.now()); + } + + public static String calculate(String template, LocalDateTime now) { + if (StringUtils.isBlank(template)) { + return template; + } + final String regex = "\\$\\{(@(" + FORMATTER_NAMES + ")(\\s*[-+]\\s*\\d+[" + UNIT_NAMES + "])*)}"; + + final Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE); + final Matcher matcher = pattern.matcher(template); + + while (matcher.find()) { + String part = matcher.group(0); + String expression = matcher.group(1); + template = template.replace(part, calculateExpression(expression, now)); + + } + return template; + } + + + private static String calculateExpression(String expression, LocalDateTime now) { + expression = expression.replaceAll("\\s", ""); + final String regex = "(@(" + FORMATTER_NAMES + "))|([-+]\\d+[" + UNIT_NAMES + "])"; + final Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE); + final Matcher matcher = pattern.matcher(expression); + List<String> elements = new ArrayList<>(); + while (matcher.find()) { + elements.add(matcher.group(0)); + } + LocalDateTime result = now; + for (int i = 1; i < elements.size(); i++) { + String element = elements.get(i); + int operator = element.charAt(0) == '-' ? -1 : 1; + int duration = Integer.parseInt(element.substring(1, element.length() - 1)); + char unit = element.charAt(element.length() - 1); + result = result.plus(operator * duration, UNIT_MAP.get(unit)); + + } + return FORMATTER.get(elements.get(0).substring(1)).apply(result); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/cache/GlobalConnectionCacheTest.java b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/cache/GlobalConnectionCacheTest.java new file mode 100644 index 0000000..d066434 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/cache/GlobalConnectionCacheTest.java @@ -0,0 +1,175 @@ +/* + * 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.collector.collect.common.cache; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link GlobalConnectionCache}. + */ +class GlobalConnectionCacheTest { + + private TestGlobalConnectionCache globalConnectionCache; + + @BeforeEach + void setUp() { + globalConnectionCache = new TestGlobalConnectionCache(); + } + + @AfterEach + void tearDown() { + if (globalConnectionCache != null) { + globalConnectionCache.destroy(); + } + } + + @Test + void dispatchCleanupCacheRunsOnVirtualThread() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + globalConnectionCache.setVirtualThreadHook(latch, virtualThread); + + globalConnectionCache.dispatchCleanupCache(); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void dispatchCleanupCacheDoesNotRunConcurrently() throws Exception { + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + AtomicInteger maxConcurrent = new AtomicInteger(); + globalConnectionCache.setConcurrencyHook(firstStarted, releaseFirst, secondStarted, maxConcurrent); + + globalConnectionCache.dispatchCleanupCache(); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + globalConnectionCache.dispatchCleanupCache(); + assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS)); + + releaseFirst.countDown(); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + assertEquals(1, maxConcurrent.get()); + } + + @Test + void dispatchCleanupCacheClosesExpiredConnections() throws Exception { + TestConnection connection = new TestConnection(); + globalConnectionCache.addCache("expired", connection, -1L); + + globalConnectionCache.dispatchCleanupCache(); + + assertTrue(connection.closed.await(5, TimeUnit.SECONDS)); + assertEquals(1, connection.closeCount.get()); + assertTrue(globalConnectionCache.getCache("expired", false).isEmpty()); + } + + private static final class TestGlobalConnectionCache extends GlobalConnectionCache { + + private CountDownLatch virtualThreadLatch; + + private AtomicBoolean virtualThread; + + private CountDownLatch firstStarted; + + private CountDownLatch releaseFirst; + + private CountDownLatch secondStarted; + + private AtomicInteger maxConcurrent; + + private final AtomicInteger concurrent = new AtomicInteger(); + + private final AtomicInteger invocations = new AtomicInteger(); + + private TestGlobalConnectionCache() { + super(false); + } + + private void setVirtualThreadHook(CountDownLatch latch, AtomicBoolean flag) { + this.virtualThreadLatch = latch; + this.virtualThread = flag; + } + + private void setConcurrencyHook(CountDownLatch firstStarted, CountDownLatch releaseFirst, + CountDownLatch secondStarted, AtomicInteger maxConcurrent) { + this.firstStarted = firstStarted; + this.releaseFirst = releaseFirst; + this.secondStarted = secondStarted; + this.maxConcurrent = maxConcurrent; + } + + @Override + void beforeCleanTimeoutOrUnHealthyCacheRun() { + if (virtualThread != null) { + virtualThread.set(Thread.currentThread().isVirtual()); + } + if (virtualThreadLatch != null) { + virtualThreadLatch.countDown(); + } + if (maxConcurrent == null) { + return; + } + int active = concurrent.incrementAndGet(); + maxConcurrent.accumulateAndGet(active, Math::max); + int currentInvocation = invocations.incrementAndGet(); + try { + if (currentInvocation == 1) { + firstStarted.countDown(); + releaseFirst.await(5, TimeUnit.SECONDS); + } else if (currentInvocation == 2) { + secondStarted.countDown(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + concurrent.decrementAndGet(); + } + } + } + + private static final class TestConnection extends AbstractConnection<Object> { + + private final CountDownLatch closed = new CountDownLatch(1); + + private final AtomicInteger closeCount = new AtomicInteger(); + + @Override + public Object getConnection() { + return new Object(); + } + + @Override + public void closeConnection() { + closeCount.incrementAndGet(); + closed.countDown(); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/http/CommonHttpClientVirtualThreadTest.java b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/http/CommonHttpClientVirtualThreadTest.java new file mode 100644 index 0000000..2ec28df --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/http/CommonHttpClientVirtualThreadTest.java @@ -0,0 +1,121 @@ +/* + * 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.collector.collect.common.http; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for CommonHttpClient cleanup dispatch. + */ +class CommonHttpClientVirtualThreadTest { + + private final PoolingHttpClientConnectionManager originalConnectionManager = CommonHttpClient.getConnectionManager(); + + @AfterEach + void tearDown() { + CommonHttpClient.setBeforeCleanupHookForTest(null); + CommonHttpClient.setConnectionManagerForTest(originalConnectionManager); + } + + @Test + void dispatchConnectionPoolCleanupRunsOnVirtualThread() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + PoolingHttpClientConnectionManager manager = mock(PoolingHttpClientConnectionManager.class); + CommonHttpClient.setConnectionManagerForTest(manager); + CommonHttpClient.setBeforeCleanupHookForTest(() -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + }); + + CommonHttpClient.dispatchConnectionPoolCleanup(); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void dispatchConnectionPoolCleanupDoesNotRunConcurrently() throws Exception { + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + AtomicInteger concurrent = new AtomicInteger(); + AtomicInteger maxConcurrent = new AtomicInteger(); + AtomicInteger invocations = new AtomicInteger(); + PoolingHttpClientConnectionManager manager = mock(PoolingHttpClientConnectionManager.class); + CommonHttpClient.setConnectionManagerForTest(manager); + CommonHttpClient.setBeforeCleanupHookForTest(() -> { + int active = concurrent.incrementAndGet(); + maxConcurrent.accumulateAndGet(active, Math::max); + int currentInvocation = invocations.incrementAndGet(); + try { + if (currentInvocation == 1) { + firstStarted.countDown(); + releaseFirst.await(5, TimeUnit.SECONDS); + } else if (currentInvocation == 2) { + secondStarted.countDown(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + concurrent.decrementAndGet(); + } + }); + + CommonHttpClient.dispatchConnectionPoolCleanup(); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + CommonHttpClient.dispatchConnectionPoolCleanup(); + assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS)); + + releaseFirst.countDown(); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + assertEquals(1, maxConcurrent.get()); + } + + @Test + void dispatchConnectionPoolCleanupClosesExpiredAndIdleConnections() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + PoolingHttpClientConnectionManager manager = mock(PoolingHttpClientConnectionManager.class); + doAnswer(invocation -> { + latch.countDown(); + return null; + }).when(manager).closeExpiredConnections(); + CommonHttpClient.setConnectionManagerForTest(manager); + + CommonHttpClient.dispatchConnectionPoolCleanup(); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + verify(manager, times(1)).closeExpiredConnections(); + verify(manager, times(1)).closeIdleConnections(40, TimeUnit.SECONDS); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/ssh/CommonSshClientTest.java b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/ssh/CommonSshClientTest.java new file mode 100644 index 0000000..bfd3eab --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/ssh/CommonSshClientTest.java @@ -0,0 +1,42 @@ +/* + * 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.collector.collect.common.ssh; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.core.CoreModuleProperties; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link CommonSshClient} + */ +class CommonSshClientTest { + + @Test + void getSshClient() { + SshClient sshClient = CommonSshClient.getSshClient(); + + assertTrue(sshClient.isStarted()); + assertEquals(Duration.ofSeconds(30), CoreModuleProperties.HEARTBEAT_INTERVAL.getRequired(sshClient)); + assertEquals(2, CoreModuleProperties.HEARTBEAT_NO_REPLY_MAX.getRequired(sshClient)); + assertTrue(CoreModuleProperties.SOCKET_KEEPALIVE.getRequired(sshClient)); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/ssh/SshCircuitBreakerTest.java b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/ssh/SshCircuitBreakerTest.java new file mode 100644 index 0000000..e1b46e8 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/ssh/SshCircuitBreakerTest.java @@ -0,0 +1,111 @@ +/* + * 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.collector.collect.common.ssh; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.common.base.Ticker; +import java.io.IOException; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link SshCircuitBreaker}. + */ +class SshCircuitBreakerTest { + + private TestTicker ticker; + private SshCircuitBreaker circuitBreaker; + private SshCircuitBreaker.Target target; + + @BeforeEach + void setUp() { + ticker = new TestTicker(); + circuitBreaker = new SshCircuitBreaker(3, Duration.ofMinutes(5), ticker); + target = circuitBreaker.buildTarget( + "127.0.0.1", "22", "root", List.of("target-password", "bad-password")); + } + + @Test + void opensAfterThresholdAndExpiresAfterCooldown() { + circuitBreaker.recordFailure(target); + circuitBreaker.recordFailure(target); + assertDoesNotThrow(() -> circuitBreaker.checkOpen(target)); + + circuitBreaker.recordFailure(target); + assertThrows(IOException.class, () -> circuitBreaker.checkOpen(target)); + + ticker.advance(Duration.ofMinutes(5).plusNanos(1)); + assertDoesNotThrow(() -> circuitBreaker.checkOpen(target)); + } + + @Test + void successAndCredentialChangeBypassPreviousFailures() { + circuitBreaker.recordFailure(target); + circuitBreaker.recordFailure(target); + circuitBreaker.recordFailure(target); + + SshCircuitBreaker.Target correctedCredentials = circuitBreaker.buildTarget( + "127.0.0.1", "22", "root", List.of("target-password", "correct-password")); + assertNotEquals(target, correctedCredentials); + assertDoesNotThrow(() -> circuitBreaker.checkOpen(correctedCredentials)); + + circuitBreaker.recordSuccess(target); + assertDoesNotThrow(() -> circuitBreaker.checkOpen(target)); + } + + @Test + void equalTargetsShareConnectionLockWithoutExposingCredentials() { + SshCircuitBreaker.Target equalTarget = circuitBreaker.buildTarget( + "127.0.0.1", "22", "root", List.of("target-password", "bad-password")); + + assertSame(circuitBreaker.connectionLock(target), circuitBreaker.connectionLock(equalTarget)); + assertEquals("root@127.0.0.1:22", target.toString()); + } + + @Test + void rejectsInvalidConfiguration() { + assertThrows(IllegalArgumentException.class, + () -> new SshCircuitBreaker(0, Duration.ofMinutes(5), ticker)); + assertThrows(IllegalArgumentException.class, + () -> new SshCircuitBreaker(3, Duration.ZERO, ticker)); + assertThrows(IllegalArgumentException.class, + () -> new SshCircuitBreaker(3, Duration.ofSeconds(-1), ticker)); + } + + private static final class TestTicker extends Ticker { + + private final AtomicLong nanos = new AtomicLong(); + + @Override + public long read() { + return nanos.get(); + } + + private void advance(Duration duration) { + nanos.addAndGet(duration.toNanos()); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/ssh/SshHelperTest.java b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/ssh/SshHelperTest.java new file mode 100644 index 0000000..36795d7 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/collect/common/ssh/SshHelperTest.java @@ -0,0 +1,551 @@ +/* + * 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.collector.collect.common.ssh; + +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.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.security.KeyPair; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.apache.hertzbeat.common.entity.job.protocol.SshProtocol; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.config.hosts.HostConfigEntry; +import org.apache.sshd.client.future.AuthFuture; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.SshConstants; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.MockedStatic; + +/** + * Tests for {@link SshHelper}. + */ +class SshHelperTest { + + private static final int TIMEOUT = 1_000; + + private String host; + private SshClient sshClient; + + @BeforeEach + void setUp() { + host = UUID.randomUUID() + ".example.com"; + sshClient = mock(SshClient.class); + } + + @Test + void blocksSameEffectiveCredentialsAndRetriesAfterPasswordChanges() throws Exception { + ConnectAttempt firstFailure = failedAttempt("first auth failure"); + ConnectAttempt secondFailure = failedAttempt("second auth failure"); + ConnectAttempt thirdFailure = failedAttempt("third auth failure"); + ConnectAttempt success = successfulAttempt(); + when(sshClient.connect("root", host, 22)).thenReturn( + firstFailure.connectFuture(), secondFailure.connectFuture(), + thirdFailure.connectFuture(), success.connectFuture()); + + SshProtocol badProtocol = protocol("bad-password", "ignored-private-key-a"); + SshProtocol samePassword = protocol("bad-password", "ignored-private-key-b"); + SshProtocol correctedProtocol = protocol("correct-password", "ignored-private-key-b"); + + try (MockedStatic<CommonSshClient> commonSshClient = mockStatic(CommonSshClient.class)) { + commonSshClient.when(CommonSshClient::getSshClient).thenReturn(sshClient); + + for (int i = 0; i < SshCircuitBreaker.MAX_FAILURES; i++) { + assertThrows(SshException.class, + () -> SshHelper.getConnectSession(badProtocol, TIMEOUT, false, false)); + } + IOException blocked = assertThrows(IOException.class, + () -> SshHelper.getConnectSession(samePassword, TIMEOUT, false, false)); + assertTrue(blocked.getMessage().contains("temporarily blocked")); + + assertSame(success.clientSession(), + SshHelper.getConnectSession(correctedProtocol, TIMEOUT, false, false)); + } + + verify(sshClient, times(4)).connect("root", host, 22); + verify(firstFailure.clientSession()).close(); + verify(secondFailure.clientSession()).close(); + verify(thirdFailure.clientSession()).close(); + verify(success.clientSession(), never()).close(); + verify(success.clientSession()).addPasswordIdentity("correct-password"); + } + + @Test + void concurrentReuseCreatesOnePhysicalSession() throws Exception { + ConnectAttempt success = successfulAttempt(); + CountDownLatch authenticationStarted = new CountDownLatch(1); + CountDownLatch allowAuthentication = new CountDownLatch(1); + when(success.authFuture().verify(TIMEOUT, TimeUnit.MILLISECONDS)).thenAnswer(invocation -> { + authenticationStarted.countDown(); + assertTrue(allowAuthentication.await(5, TimeUnit.SECONDS)); + return success.authFuture(); + }); + when(success.clientSession().isClosed()).thenReturn(false); + when(success.clientSession().isClosing()).thenReturn(false); + when(sshClient.connect("root", host, 22)).thenReturn(success.connectFuture()); + + CountDownLatch secondStarted = new CountDownLatch(1); + try (ExecutorService executor = Executors.newFixedThreadPool(2)) { + Future<ClientSession> first = executor.submit(() -> connectWithThreadLocalClient("password")); + assertTrue(authenticationStarted.await(5, TimeUnit.SECONDS)); + Future<ClientSession> second = executor.submit(() -> { + secondStarted.countDown(); + return connectWithThreadLocalClient("password"); + }); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + + assertThrows(TimeoutException.class, () -> second.get(200, TimeUnit.MILLISECONDS)); + verify(sshClient).connect("root", host, 22); + allowAuthentication.countDown(); + assertSame(success.clientSession(), first.get(5, TimeUnit.SECONDS)); + assertSame(success.clientSession(), second.get(5, TimeUnit.SECONDS)); + } finally { + allowAuthentication.countDown(); + } + + verify(sshClient).connect("root", host, 22); + verify(success.clientSession()).auth(); + verify(success.authFuture()).verify(TIMEOUT, TimeUnit.MILLISECONDS); + } + + @Test + void concurrentAuthenticationFailuresCreateAtMostThreePhysicalSessions() throws Exception { + ConnectAttempt firstFailure = failedAttempt("first concurrent failure"); + ConnectAttempt secondFailure = failedAttempt("second concurrent failure"); + ConnectAttempt thirdFailure = failedAttempt("third concurrent failure"); + when(sshClient.connect("root", host, 22)).thenReturn( + firstFailure.connectFuture(), secondFailure.connectFuture(), thirdFailure.connectFuture()); + + int taskCount = 8; + CountDownLatch ready = new CountDownLatch(taskCount); + CountDownLatch start = new CountDownLatch(1); + List<Future<Exception>> futures = new ArrayList<>(); + try (ExecutorService executor = Executors.newFixedThreadPool(taskCount)) { + for (int i = 0; i < taskCount; i++) { + futures.add(executor.submit(() -> { + ready.countDown(); + assertTrue(start.await(5, TimeUnit.SECONDS)); + try { + connectWithThreadLocalClient("concurrent-bad-password", false); + return null; + } catch (Exception e) { + return e; + } + })); + } + assertTrue(ready.await(5, TimeUnit.SECONDS)); + start.countDown(); + } + + int authenticationFailures = 0; + int blockedFailures = 0; + for (Future<Exception> future : futures) { + Exception exception = future.get(5, TimeUnit.SECONDS); + if (exception instanceof SshException) { + authenticationFailures++; + } else { + assertTrue(exception instanceof IOException); + assertTrue(exception.getMessage().contains("temporarily blocked")); + blockedFailures++; + } + } + assertEquals(SshCircuitBreaker.MAX_FAILURES, authenticationFailures); + assertEquals(taskCount - SshCircuitBreaker.MAX_FAILURES, blockedFailures); + verify(sshClient, times(SshCircuitBreaker.MAX_FAILURES)).connect("root", host, 22); + verify(firstFailure.clientSession()).close(); + verify(secondFailure.clientSession()).close(); + verify(thirdFailure.clientSession()).close(); + } + + @Test + void proxyFailuresOpenCircuitAndRemoveGlobalPassword() throws Exception { + ConnectFuture connectFuture = mock(ConnectFuture.class); + ConnectAttempt correctedSuccess = successfulAttempt(); + when(sshClient.connect(any(HostConfigEntry.class))).thenReturn( + connectFuture, connectFuture, connectFuture, correctedSuccess.connectFuture()); + when(connectFuture.verify(TIMEOUT, TimeUnit.MILLISECONDS)) + .thenThrow(new SshException( + SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, "proxy auth failed")); + SshProtocol protocol = proxyProtocol("bad-proxy-password", "target-password"); + SshProtocol otherTarget = proxyProtocol("bad-proxy-password", "different-target-password"); + otherTarget.setHost(UUID.randomUUID() + ".example.com"); + SshProtocol correctedProtocol = proxyProtocol("correct-proxy-password", "target-password"); + + try (MockedStatic<CommonSshClient> commonSshClient = mockStatic(CommonSshClient.class)) { + commonSshClient.when(CommonSshClient::getSshClient).thenReturn(sshClient); + + for (int i = 0; i < SshCircuitBreaker.MAX_FAILURES; i++) { + assertThrows(SshException.class, + () -> SshHelper.getConnectSession(protocol, TIMEOUT, false, true)); + } + IOException blocked = assertThrows(IOException.class, + () -> SshHelper.getConnectSession(protocol, TIMEOUT, false, true)); + assertTrue(blocked.getMessage().contains("temporarily blocked")); + IOException sharedProxyBlocked = assertThrows(IOException.class, + () -> SshHelper.getConnectSession(otherTarget, TIMEOUT, false, true)); + assertTrue(sharedProxyBlocked.getMessage().contains("temporarily blocked")); + + assertSame(correctedSuccess.clientSession(), + SshHelper.getConnectSession(correctedProtocol, TIMEOUT, false, true)); + } + + verify(sshClient, times(SshCircuitBreaker.MAX_FAILURES + 1)).connect(any(HostConfigEntry.class)); + verify(sshClient, times(SshCircuitBreaker.MAX_FAILURES)) + .addPasswordIdentity("bad-proxy-password"); + verify(sshClient, times(SshCircuitBreaker.MAX_FAILURES)) + .removePasswordIdentity("bad-proxy-password"); + verify(sshClient).addPasswordIdentity("correct-proxy-password"); + verify(sshClient).removePasswordIdentity("correct-proxy-password"); + } + + @Test + void cachedProxySessionDoesNotResetProxyAuthenticationFailures() throws Exception { + ConnectAttempt cachedSuccess = successfulAttempt(); + ConnectFuture failedProxyConnect = mock(ConnectFuture.class); + when(failedProxyConnect.verify(TIMEOUT, TimeUnit.MILLISECONDS)) + .thenThrow(new SshException( + SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, "proxy auth failed")); + when(cachedSuccess.clientSession().isClosed()).thenReturn(false); + when(cachedSuccess.clientSession().isClosing()).thenReturn(false); + when(sshClient.connect(any(HostConfigEntry.class))).thenReturn( + cachedSuccess.connectFuture(), failedProxyConnect, failedProxyConnect, failedProxyConnect); + String proxyPassword = UUID.randomUUID() + "-proxy-password"; + SshProtocol cachedProtocol = proxyProtocol(proxyPassword, "cached-target-password"); + SshProtocol failingProtocol = proxyProtocol(proxyPassword, "failing-target-password"); + failingProtocol.setHost(UUID.randomUUID() + ".example.com"); + + try (MockedStatic<CommonSshClient> commonSshClient = mockStatic(CommonSshClient.class)) { + commonSshClient.when(CommonSshClient::getSshClient).thenReturn(sshClient); + + assertSame(cachedSuccess.clientSession(), + SshHelper.getConnectSession(cachedProtocol, TIMEOUT, false, true)); + for (int i = 0; i < SshCircuitBreaker.MAX_FAILURES; i++) { + assertThrows(SshException.class, + () -> SshHelper.getConnectSession(failingProtocol, TIMEOUT, false, true)); + assertSame(cachedSuccess.clientSession(), + SshHelper.getConnectSession(cachedProtocol, TIMEOUT, false, true)); + } + IOException blocked = assertThrows(IOException.class, + () -> SshHelper.getConnectSession(failingProtocol, TIMEOUT, false, true)); + assertTrue(blocked.getMessage().contains("temporarily blocked")); + } + + verify(sshClient, times(SshCircuitBreaker.MAX_FAILURES + 1)) + .connect(any(HostConfigEntry.class)); + } + + @Test + void proxyTransportFailuresDoNotOpenAuthenticationCircuit() throws Exception { + ConnectFuture connectFuture = mock(ConnectFuture.class); + when(sshClient.connect(any(HostConfigEntry.class))).thenReturn(connectFuture); + when(connectFuture.verify(TIMEOUT, TimeUnit.MILLISECONDS)) + .thenThrow(new SshException( + SshConstants.SSH2_DISCONNECT_CONNECTION_LOST, "proxy transport failed")); + String proxyPassword = UUID.randomUUID() + "-proxy-password"; + SshProtocol protocol = proxyProtocol(proxyPassword, "target-password"); + + try (MockedStatic<CommonSshClient> commonSshClient = mockStatic(CommonSshClient.class)) { + commonSshClient.when(CommonSshClient::getSshClient).thenReturn(sshClient); + + for (int i = 0; i < SshCircuitBreaker.MAX_FAILURES + 1; i++) { + assertThrows(SshException.class, + () -> SshHelper.getConnectSession(protocol, TIMEOUT, false, true)); + } + } + + verify(sshClient, times(SshCircuitBreaker.MAX_FAILURES + 1)) + .connect(any(HostConfigEntry.class)); + verify(sshClient, times(SshCircuitBreaker.MAX_FAILURES + 1)) + .addPasswordIdentity(proxyPassword); + verify(sshClient, times(SshCircuitBreaker.MAX_FAILURES + 1)) + .removePasswordIdentity(proxyPassword); + } + + @Test + void proxyPasswordIsRemovedBeforeTargetAuthentication() throws Exception { + ConnectAttempt success = successfulAttempt(); + when(sshClient.connect(any(HostConfigEntry.class))).thenReturn(success.connectFuture()); + SshProtocol protocol = proxyProtocol("proxy-password", "target-password"); + + try (MockedStatic<CommonSshClient> commonSshClient = mockStatic(CommonSshClient.class)) { + commonSshClient.when(CommonSshClient::getSshClient).thenReturn(sshClient); + + assertSame(success.clientSession(), + SshHelper.getConnectSession(protocol, TIMEOUT, false, true)); + } + + verify(sshClient).addPasswordIdentity("proxy-password"); + verify(sshClient).connect(any(HostConfigEntry.class)); + verify(success.connectFuture()).verify(TIMEOUT, TimeUnit.MILLISECONDS); + verify(sshClient).removePasswordIdentity("proxy-password"); + + InOrder order = inOrder(success.clientSession(), success.authFuture()); + order.verify(success.clientSession()).removePasswordIdentity("proxy-password"); + order.verify(success.clientSession()).addPasswordIdentity("target-password"); + order.verify(success.clientSession()).auth(); + order.verify(success.authFuture()).verify(TIMEOUT, TimeUnit.MILLISECONDS); + + ArgumentCaptor<HostConfigEntry> configCaptor = ArgumentCaptor.forClass(HostConfigEntry.class); + verify(sshClient).connect(configCaptor.capture()); + HostConfigEntry proxyConfig = configCaptor.getValue(); + assertEquals(host, proxyConfig.getHostName()); + assertEquals("root", proxyConfig.getUsername()); + assertEquals("proxy-user@proxy.example.com:2222", proxyConfig.getProxyJump()); + } + + @Test + void defaultIdentityReconnectsAfterCachedSessionsBecomeUnhealthy() throws Exception { + ConnectAttempt first = successfulAttempt(); + ConnectAttempt second = successfulAttempt(); + ConnectAttempt third = successfulAttempt(); + when(sshClient.connect("root", host, 22)).thenReturn( + first.connectFuture(), second.connectFuture(), third.connectFuture()); + + try (MockedStatic<CommonSshClient> commonSshClient = mockStatic(CommonSshClient.class)) { + commonSshClient.when(CommonSshClient::getSshClient).thenReturn(sshClient); + + assertSame(first.clientSession(), SshHelper.getConnectSession( + host, "22", "root", null, null, null, TIMEOUT, true)); + when(first.clientSession().isClosed()).thenReturn(true); + assertSame(second.clientSession(), SshHelper.getConnectSession( + host, "22", "root", null, null, null, TIMEOUT, true)); + when(second.clientSession().isClosed()).thenThrow(new IllegalStateException("health check failed")); + assertSame(third.clientSession(), SshHelper.getConnectSession( + host, "22", "root", null, null, null, TIMEOUT, true)); + when(third.clientSession().isClosed()).thenReturn(false); + when(third.clientSession().isClosing()).thenReturn(false); + assertSame(third.clientSession(), SshHelper.getConnectSession( + host, "22", "root", null, null, null, TIMEOUT, true)); + } + + verify(sshClient, times(3)).connect("root", host, 22); + verify(first.clientSession()).close(); + verify(second.clientSession()).close(); + verify(third.clientSession(), never()).close(); + verify(first.clientSession(), never()).addPasswordIdentity(any()); + verify(first.clientSession(), never()).addPublicKeyIdentity(any()); + } + + @Test + void loadsTargetPrivateKeyDirectlyFromMemory() throws Exception { + ConnectAttempt success = successfulAttempt(); + KeyPair keyPair = mock(KeyPair.class); + when(sshClient.connect("root", host, 22)).thenReturn(success.connectFuture()); + + try (MockedStatic<CommonSshClient> commonSshClient = mockStatic(CommonSshClient.class); + MockedStatic<SecurityUtils> securityUtils = mockStatic(SecurityUtils.class)) { + commonSshClient.when(CommonSshClient::getSshClient).thenReturn(sshClient); + securityUtils.when(() -> SecurityUtils.loadKeyPairIdentities(any(), any(), any(), any())) + .thenAnswer(invocation -> { + FilePasswordProvider provider = invocation.getArgument(3); + assertEquals("passphrase", provider.getPassword(null, null, 0)); + return List.of(keyPair); + }); + + assertSame(success.clientSession(), SshHelper.getConnectSession( + host, "22", "root", null, "private-key", "passphrase", TIMEOUT, false)); + } + + verify(success.clientSession()).addPublicKeyIdentity(keyPair); + verify(success.clientSession(), never()).addPasswordIdentity(any()); + } + + @Test + void proxyPrivateKeyIsRemovedBeforeTargetAuthentication() throws Exception { + ConnectAttempt success = successfulAttempt(); + KeyPair keyPair = mock(KeyPair.class); + when(sshClient.connect(any(HostConfigEntry.class))).thenReturn(success.connectFuture()); + SshProtocol protocol = proxyProtocol(null, "target-password"); + protocol.setProxyPrivateKey("proxy-private-key"); + + try (MockedStatic<CommonSshClient> commonSshClient = mockStatic(CommonSshClient.class); + MockedStatic<SecurityUtils> securityUtils = mockStatic(SecurityUtils.class)) { + commonSshClient.when(CommonSshClient::getSshClient).thenReturn(sshClient); + securityUtils.when(() -> SecurityUtils.loadKeyPairIdentities(any(), any(), any(), any())) + .thenAnswer(invocation -> { + FilePasswordProvider provider = invocation.getArgument(3); + assertNull(provider.getPassword(null, null, 0)); + return List.of(keyPair); + }); + + assertSame(success.clientSession(), + SshHelper.getConnectSession(protocol, TIMEOUT, false, true)); + } + + verify(sshClient).addPublicKeyIdentity(keyPair); + verify(sshClient).removePublicKeyIdentity(keyPair); + verify(success.clientSession()).removePublicKeyIdentity(keyPair); + verify(sshClient, never()).addPasswordIdentity(any()); + } + + @Test + void connectionLockInterruptionIsPropagated() { + Thread.currentThread().interrupt(); + try { + InterruptedIOException exception = assertThrows(InterruptedIOException.class, + () -> SshHelper.getConnectSession( + host, "22", "root", "password", null, null, TIMEOUT, true)); + + assertTrue(exception.getMessage().contains("SSH connection")); + assertTrue(exception.getCause() instanceof InterruptedException); + assertTrue(Thread.currentThread().isInterrupted()); + } finally { + Thread.interrupted(); + } + } + + @Test + void interruptedAuthenticationForcesSessionCloseAndPreservesInterrupt() throws Exception { + ConnectAttempt attempt = attempt(); + InterruptedIOException interrupted = new InterruptedIOException("authentication interrupted"); + when(attempt.authFuture().verify(TIMEOUT, TimeUnit.MILLISECONDS)).thenThrow(interrupted); + when(sshClient.connect("root", host, 22)).thenReturn(attempt.connectFuture()); + + try (MockedStatic<CommonSshClient> commonSshClient = mockStatic(CommonSshClient.class)) { + commonSshClient.when(CommonSshClient::getSshClient).thenReturn(sshClient); + try { + assertSame(interrupted, assertThrows(InterruptedIOException.class, + () -> SshHelper.getConnectSession( + host, "22", "root", "password", null, null, TIMEOUT, false))); + assertTrue(Thread.currentThread().isInterrupted()); + verify(attempt.clientSession()).close(true); + verify(attempt.clientSession(), never()).close(); + } finally { + Thread.interrupted(); + } + } + } + + @Test + void closeFailureDoesNotHideAuthenticationFailure() throws Exception { + ConnectAttempt attempt = attempt(); + SshException authenticationFailure = new SshException( + SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, "auth failed"); + InterruptedIOException closeFailure = new InterruptedIOException("close interrupted"); + when(attempt.authFuture().verify(TIMEOUT, TimeUnit.MILLISECONDS)).thenThrow(authenticationFailure); + doThrow(closeFailure).when(attempt.clientSession()).close(); + when(sshClient.connect("root", host, 22)).thenReturn(attempt.connectFuture()); + + try (MockedStatic<CommonSshClient> commonSshClient = mockStatic(CommonSshClient.class)) { + commonSshClient.when(CommonSshClient::getSshClient).thenReturn(sshClient); + try { + SshException thrown = assertThrows(SshException.class, + () -> SshHelper.getConnectSession( + host, "22", "root", "password", null, null, TIMEOUT, false)); + assertSame(authenticationFailure, thrown); + assertEquals(1, thrown.getSuppressed().length); + assertSame(closeFailure, thrown.getSuppressed()[0]); + assertTrue(Thread.currentThread().isInterrupted()); + } finally { + Thread.interrupted(); + } + } + } + + private ClientSession connectWithThreadLocalClient(String password) throws Exception { + return connectWithThreadLocalClient(password, true); + } + + private ClientSession connectWithThreadLocalClient(String password, boolean reuseConnection) throws Exception { + try (MockedStatic<CommonSshClient> commonSshClient = mockStatic(CommonSshClient.class)) { + commonSshClient.when(CommonSshClient::getSshClient).thenReturn(sshClient); + return SshHelper.getConnectSession( + host, "22", "root", password, null, null, TIMEOUT, reuseConnection); + } + } + + private SshProtocol protocol(String password, String privateKey) { + return SshProtocol.builder() + .host(host) + .port("22") + .username("root") + .password(password) + .privateKey(privateKey) + .build(); + } + + private SshProtocol proxyProtocol(String proxyPassword, String targetPassword) { + return SshProtocol.builder() + .host(host) + .port("22") + .username("root") + .password(targetPassword) + .proxyHost("proxy.example.com") + .proxyPort("2222") + .proxyUsername("proxy-user") + .proxyPassword(proxyPassword) + .build(); + } + + private ConnectAttempt failedAttempt(String message) throws IOException { + ConnectAttempt attempt = attempt(); + when(attempt.authFuture().verify(TIMEOUT, TimeUnit.MILLISECONDS)) + .thenThrow(new SshException( + SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, message)); + return attempt; + } + + private ConnectAttempt successfulAttempt() throws IOException { + ConnectAttempt attempt = attempt(); + when(attempt.authFuture().verify(TIMEOUT, TimeUnit.MILLISECONDS)) + .thenReturn(attempt.authFuture()); + return attempt; + } + + private ConnectAttempt attempt() throws IOException { + ConnectFuture connectFuture = mock(ConnectFuture.class); + ClientSession clientSession = mock(ClientSession.class); + AuthFuture authFuture = mock(AuthFuture.class); + when(connectFuture.verify(TIMEOUT, TimeUnit.MILLISECONDS)).thenReturn(connectFuture); + when(connectFuture.getSession()).thenReturn(clientSession); + when(clientSession.auth()).thenReturn(authFuture); + return new ConnectAttempt(connectFuture, clientSession, authFuture); + } + + private record ConnectAttempt( + ConnectFuture connectFuture, ClientSession clientSession, AuthFuture authFuture) { + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/GoOnlineProcessorTest.java b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/GoOnlineProcessorTest.java new file mode 100644 index 0000000..b544c8c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/dispatch/entrance/processor/GoOnlineProcessorTest.java @@ -0,0 +1,96 @@ +/* + * 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.collector.dispatch.entrance.processor; + +import com.google.common.collect.Lists; +import com.google.protobuf.ByteString; +import io.netty.channel.ChannelHandlerContext; +import org.apache.hertzbeat.collector.timer.TimerDispatcher; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Field; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test for GoOnlineProcessor + */ +class GoOnlineProcessorTest { + + private GoOnlineProcessor goOnlineProcessor; + private TimerDispatcher timerDispatcher; + + @Mock + private ChannelHandlerContext channelHandlerContext; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + timerDispatcher = new TimerDispatcher(); + goOnlineProcessor = new GoOnlineProcessor(timerDispatcher); + } + + @AfterEach + void tearDown() throws Exception { + timerDispatcher.destroy(); + } + + @Test + void verifyTaskMapPreservation() throws Exception { + Job job = Job.builder() + .app("test") + .id(12345L) + .metrics(Lists.newArrayList(Metrics.builder().interval(100L).build())) + .configmap(Lists.newArrayList()) + .isCyclic(true) + .build(); + timerDispatcher.addJob(job, null); + + Field cyclicTaskMapField = TimerDispatcher.class.getDeclaredField("currentCyclicTaskMap"); + cyclicTaskMapField.setAccessible(true); + Map<?, ?> currentCyclicTaskMap = (Map<?, ?>) cyclicTaskMapField.get(timerDispatcher); + assertEquals(1, currentCyclicTaskMap.size(), "Task map should have 1 job initially"); + + ClusterMsg.Message responseMsg = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.GO_ONLINE) + .setDirection(ClusterMsg.Direction.RESPONSE) + .setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(job))) + .setIdentity("test-identity") + .build(); + goOnlineProcessor.handle(channelHandlerContext, responseMsg); + assertEquals(1, currentCyclicTaskMap.size(), "Task map should still have 1 job after receiving RESPONSE"); + + ClusterMsg.Message requestMsg = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.GO_ONLINE) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(job))) + .setIdentity("test-identity") + .build(); + goOnlineProcessor.handle(channelHandlerContext, requestMsg); + assertEquals(0, currentCyclicTaskMap.size(), "Task map should be empty after receiving REQUEST"); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/TimerDispatcherTest.java b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/TimerDispatcherTest.java new file mode 100644 index 0000000..17cd06a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/TimerDispatcherTest.java @@ -0,0 +1,302 @@ +/* + * 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.collector.timer; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.apache.hertzbeat.collector.constants.ScheduleTypeEnum; +import org.apache.hertzbeat.collector.dispatch.MetricsTaskDispatch; +import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectResponseEventListener; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.timer.Timeout; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * Test class for TimerDispatcher's getNextExecutionInterval method + */ +public class TimerDispatcherTest { + + @InjectMocks + private TimerDispatcher timerDispatcher; + + @Mock + private Job job; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + /** + * Test getNextExecutionInterval with valid CRON expression + * Should return the calculated interval based on cron expression + */ + @Test + void testGetNextExecutionIntervalWithValidCron() { + // Setup - Use a cron expression that runs every minute + when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType()); + when(job.getCronExpression()).thenReturn("0 * * * * ?"); + when(job.getInterval()).thenReturn(60L); // Default interval as fallback + + // Execute + Long result = timerDispatcher.getNextExecutionInterval(job); + + // Verify - Result should be between 0 and 60 seconds + // since the cron expression runs every minute + assertEquals(true, result >= 0 && result <= 60); + } + + /** + * Test getNextExecutionInterval with invalid CRON expression + * Should fall back to interval scheduling + */ + @Test + void testGetNextExecutionIntervalWithInvalidCron() { + // Setup - Use an invalid cron expression + when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType()); + when(job.getCronExpression()).thenReturn("invalid-cron-expression"); + when(job.getInterval()).thenReturn(300L); // 5 minutes + + // Execute + Long result = timerDispatcher.getNextExecutionInterval(job); + + // Verify - Should fall back to interval value + assertEquals(300L, result); + } + + /** + * Test getNextExecutionInterval with empty CRON expression + * Should fall back to interval scheduling + */ + @Test + void testGetNextExecutionIntervalWithEmptyCron() { + // Setup - Use empty cron expression + when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType()); + when(job.getCronExpression()).thenReturn(""); + when(job.getInterval()).thenReturn(120L); // 2 minutes + + // Execute + Long result = timerDispatcher.getNextExecutionInterval(job); + + // Verify - Should fall back to interval value + assertEquals(120L, result); + } + + /** + * Test getNextExecutionInterval with INTERVAL schedule type and no dispatchTime + * Should return the full interval value + */ + @Test + void testGetNextExecutionIntervalWithIntervalNoDispatchTime() { + // Setup - Use interval schedule type with no dispatchTime + when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.INTERVAL.getType()); + when(job.getDispatchTime()).thenReturn(0L); // No dispatchTime set + when(job.getInterval()).thenReturn(600L); // 10 minutes + + // Execute + Long result = timerDispatcher.getNextExecutionInterval(job); + + // Verify - Should return the full interval value + assertEquals(600L, result); + } + + /** + * Test getNextExecutionInterval with INTERVAL schedule type and dispatchTime + * Should return the remaining interval time + */ + @Test + void testGetNextExecutionIntervalWithIntervalAndDispatchTime() { + // Setup - Use interval schedule type with dispatchTime + long interval = 600L; // 10 minutes in seconds + long spendTime = 300000L; // 5 minutes in milliseconds + long dispatchTime = System.currentTimeMillis() - spendTime; + + when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.INTERVAL.getType()); + when(job.getDispatchTime()).thenReturn(dispatchTime); + when(job.getInterval()).thenReturn(interval); + + // Execute + Long result = timerDispatcher.getNextExecutionInterval(job); + + // Verify - Should return the remaining interval time (approximately 5 minutes) + // Considering possible time differences during execution, we check a range + assertEquals(true, result >= 295 && result <= 305); + } + + /** + * Test getNextExecutionInterval with INTERVAL schedule type and negative remaining time + * Should return 0 to ensure non-negative value + */ + @Test + void testGetNextExecutionIntervalWithIntervalAndNegativeRemainingTime() { + // Setup - Use interval schedule type with dispatchTime resulting in negative remaining time + long interval = 600L; // 10 minutes in seconds + long spendTime = 1200000L; // 20 minutes in milliseconds (more than interval) + long dispatchTime = System.currentTimeMillis() - spendTime; + + when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.INTERVAL.getType()); + when(job.getDispatchTime()).thenReturn(dispatchTime); + when(job.getInterval()).thenReturn(interval); + + // Execute + Long result = timerDispatcher.getNextExecutionInterval(job); + + // Verify - Should return 0 to ensure non-negative value + assertEquals(0L, result); + } + + /** + * Test getNextExecutionInterval with CRON schedule type and null cronExpression + * Should fall back to interval scheduling + */ + @Test + void testGetNextExecutionIntervalWithCronAndNullExpression() { + // Setup - Use cron schedule type with null cronExpression + when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType()); + when(job.getCronExpression()).thenReturn(null); + when(job.getInterval()).thenReturn(180L); // 3 minutes + + // Execute + Long result = timerDispatcher.getNextExecutionInterval(job); + + // Verify - Should fall back to interval value + assertEquals(180L, result); + } + + @Test + void testAddCyclicJobCancelsPreviousTimeoutForSameJob() { + Job firstJob = Job.builder() + .id(1L) + .app("test") + .isCyclic(true) + .configmap(List.of()) + .metrics(List.of(Metrics.builder().interval(60L).build())) + .build(); + timerDispatcher.addJob(firstJob, null); + + Timeout firstTimeout = currentCyclicTaskMap().get(1L); + + Job updatedJob = Job.builder() + .id(1L) + .app("test") + .isCyclic(true) + .configmap(List.of()) + .metrics(List.of(Metrics.builder().interval(60L).build())) + .build(); + timerDispatcher.addJob(updatedJob, null); + + Timeout updatedTimeout = currentCyclicTaskMap().get(1L); + assertNotSame(firstTimeout, updatedTimeout); + assertTrue(firstTimeout.isCancelled()); + assertFalse(updatedTimeout.isCancelled()); + } + + @Test + void testAddTemporaryJobCancelsPreviousTimeoutForSameJob() { + Job firstJob = Job.builder() + .id(1L) + .app("test") + .isCyclic(false) + .configmap(List.of()) + .metrics(List.of(Metrics.builder().interval(60L).build())) + .build(); + timerDispatcher.addJob(firstJob, new CollectResponseEventListener() { + }); + + Timeout firstTimeout = currentTempTaskMap().get(1L); + + Job updatedJob = Job.builder() + .id(1L) + .app("test") + .isCyclic(false) + .configmap(List.of()) + .metrics(List.of(Metrics.builder().interval(60L).build())) + .build(); + timerDispatcher.addJob(updatedJob, new CollectResponseEventListener() { + }); + + Timeout updatedTimeout = currentTempTaskMap().get(1L); + assertNotSame(firstTimeout, updatedTimeout); + assertTrue(firstTimeout.isCancelled()); + assertFalse(updatedTimeout.isCancelled()); + } + + @Test + void testCyclicJobCancelsPreviousTimeoutForSameJob() { + Job job = Job.builder() + .id(1L) + .app("test") + .isCyclic(true) + .configmap(List.of()) + .metrics(List.of(Metrics.builder().interval(60L).build())) + .build(); + timerDispatcher.addJob(job, null); + Timeout t0 = currentCyclicTaskMap().get(1L); + + WheelTimerTask timerTask = new WheelTimerTask(job, (MetricsTaskDispatch) timeout -> { + }); + + timerDispatcher.cyclicJob(timerTask, 60L, TimeUnit.SECONDS); + Timeout t1 = currentCyclicTaskMap().get(1L); + + timerDispatcher.cyclicJob(timerTask, 60L, TimeUnit.SECONDS); + Timeout t2 = currentCyclicTaskMap().get(1L); + + assertNotSame(t0, t1); + assertNotSame(t1, t2); + assertFalse(t2.isCancelled()); + + assertTrue(t0.isCancelled(), "T0 orphaned: overwritten by cyclicJob but never cancelled"); + assertTrue(t1.isCancelled(), "T1 orphaned: overwritten by cyclicJob but never cancelled"); + } + + @SuppressWarnings("unchecked") + private Map<Long, Timeout> currentCyclicTaskMap() { + try { + Field field = TimerDispatcher.class.getDeclaredField("currentCyclicTaskMap"); + field.setAccessible(true); + return (Map<Long, Timeout>) field.get(timerDispatcher); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + @SuppressWarnings("unchecked") + private Map<Long, Timeout> currentTempTaskMap() { + try { + Field field = TimerDispatcher.class.getDeclaredField("currentTempTaskMap"); + field.setAccessible(true); + return (Map<Long, Timeout>) field.get(timerDispatcher); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-kafka/pom.xml b/hertzbeat-collector/hertzbeat-collector-kafka/pom.xml new file mode 100644 index 0000000..7175050 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-kafka/pom.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-collector-kafka</artifactId> + <name>${project.artifactId}</name> + + <properties> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + + <dependencies> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-common</artifactId> + <scope>provided</scope> + </dependency> + <!-- kafka --> + <dependency> + <groupId>org.apache.kafka</groupId> + <artifactId>kafka-clients</artifactId> + </dependency> + </dependencies> +</project> diff --git a/hertzbeat-collector/hertzbeat-collector-kafka/src/main/java/org/apache/hertzbeat/collector/collect/kafka/KafkaCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-kafka/src/main/java/org/apache/hertzbeat/collector/collect/kafka/KafkaCollectImpl.java new file mode 100644 index 0000000..b4f9130 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-kafka/src/main/java/org/apache/hertzbeat/collector/collect/kafka/KafkaCollectImpl.java @@ -0,0 +1,396 @@ +/* + * 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.collector.collect.kafka; + +import lombok.extern.slf4j.Slf4j; + +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection; +import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier; +import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache; +import org.apache.hertzbeat.collector.collect.kafka.constants.InternalTopic; +import org.apache.hertzbeat.collector.collect.kafka.constants.SupportedCommand; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.KafkaProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.AdminClientConfig; +import org.apache.kafka.clients.admin.ConsumerGroupDescription; +import org.apache.kafka.clients.admin.ConsumerGroupListing; +import org.apache.kafka.clients.admin.DescribeConsumerGroupsResult; +import org.apache.kafka.clients.admin.DescribeTopicsResult; +import org.apache.kafka.clients.admin.KafkaAdminClient; +import org.apache.kafka.clients.admin.ListConsumerGroupOffsetsResult; +import org.apache.kafka.clients.admin.ListConsumerGroupsResult; +import org.apache.kafka.clients.admin.ListOffsetsResult.ListOffsetsResultInfo; +import org.apache.kafka.clients.admin.ListTopicsOptions; +import org.apache.kafka.clients.admin.ListTopicsResult; +import org.apache.kafka.clients.admin.OffsetSpec; +import org.apache.kafka.clients.admin.TopicDescription; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.TopicPartitionInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +@Slf4j +public class KafkaCollectImpl extends AbstractCollect { + + private static final String LAG_NUM = "lag_num"; + private static final String PARTITION_OFFSET = "Partition_offset"; + + private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance(); + + /** + * Collect the list of topics + * + * @param builder The MetricsData builder + * @param adminClient The AdminClient + */ + private static void collectTopicList(CollectRep.MetricsData.Builder builder, AdminClient adminClient, Boolean monitorInternalTopic) throws InterruptedException, ExecutionException { + ListTopicsOptions options = new ListTopicsOptions().listInternal(true); + Set<String> topicNames = adminClient.listTopics(options).names().get(); + topicNames.forEach(topicName -> { + if (filterInternalTopics(topicName, monitorInternalTopic)) { + CollectRep.ValueRow valueRow = CollectRep.ValueRow.newBuilder().addColumn(topicName).build(); + builder.addValueRow(valueRow); + } + }); + } + + /** + * Collect the description of each topic + * + * @param builder The MetricsData builder + * @param adminClient The AdminClient + */ + private static void collectTopicDescribe(CollectRep.MetricsData.Builder builder, AdminClient adminClient, Boolean monitorInternalTopic) throws InterruptedException, ExecutionException { + ListTopicsOptions options = new ListTopicsOptions(); + options.listInternal(true); + ListTopicsResult listTopicsResult = adminClient.listTopics(options); + Set<String> names = listTopicsResult.names().get(); + DescribeTopicsResult describeTopicsResult = adminClient.describeTopics(names); + Map<String, TopicDescription> topicDescriptionMap = describeTopicsResult.all().get().entrySet().stream() + .filter(entry -> filterInternalTopics(entry.getKey(), monitorInternalTopic)) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + topicDescriptionMap.forEach((key, value) -> { + List<TopicPartitionInfo> listp = value.partitions(); + listp.forEach(info -> { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + valueRowBuilder.addColumn(value.name()); + valueRowBuilder.addColumn(String.valueOf(value.partitions().size())); + valueRowBuilder.addColumn(String.valueOf(info.partition())); + valueRowBuilder.addColumn(info.leader().host()); + valueRowBuilder.addColumn(String.valueOf(info.leader().port())); + valueRowBuilder.addColumn(String.valueOf(info.replicas().size())); + valueRowBuilder.addColumn(String.valueOf(info.replicas())); + builder.addValueRow(valueRowBuilder.build()); + }); + }); + } + + /** + * Collect Topic ConsumerGroups Message + * + * @param builder The MetricsData builder + * @param adminClient The AdminClient + */ + private static void collectTopicConsumerGroups(CollectRep.MetricsData.Builder builder, AdminClient adminClient, Boolean monitorInternalTopic) throws InterruptedException, ExecutionException { + ListTopicsOptions options = new ListTopicsOptions(); + options.listInternal(true); + // Get all consumer groups + ListConsumerGroupsResult consumerGroupsResult = adminClient.listConsumerGroups(); + Collection<ConsumerGroupListing> consumerGroups = consumerGroupsResult.all().get(); + // Get the list of consumer groups for each topic + Map<String, Set<String>> topicConsumerGroupsMap = getTopicConsumerGroupsMap(consumerGroups, adminClient); + topicConsumerGroupsMap.entrySet().stream() + .flatMap(entry -> entry.getValue().stream() + .map(groupId -> { + try { + String topicName = entry.getKey(); + if (filterInternalTopics(topicName, monitorInternalTopic)) { + DescribeConsumerGroupsResult describeResult = adminClient.describeConsumerGroups(Collections.singletonList(groupId)); + Map<String, ConsumerGroupDescription> consumerGroupDescriptions = describeResult.all().get(); + ConsumerGroupDescription description = consumerGroupDescriptions.get(groupId); + Map<String, String> offsetAndLagNum = getConsumerGroupMetrics(topicName, groupId, adminClient); + return CollectRep.ValueRow.newBuilder() + .addColumn(groupId) + .addColumn(String.valueOf(description.members().size())) + .addColumn(topicName) + .addColumn(offsetAndLagNum.get(PARTITION_OFFSET)) + .addColumn(offsetAndLagNum.get(LAG_NUM)) + .build(); + } + } catch (InterruptedException | ExecutionException e) { + log.warn("group {} get message fail", groupId); + } + return null; + }) + ) + .filter(Objects::nonNull) + .forEach(builder::addValueRow); + } + + private static Map<String, Set<String>> getTopicConsumerGroupsMap(Collection<ConsumerGroupListing> consumerGroups, + AdminClient adminClient) + throws ExecutionException, InterruptedException { + Map<String, Set<String>> topicConsumerGroupsMap = new HashMap<>(); + for (ConsumerGroupListing consumerGroup : consumerGroups) { + String groupId = consumerGroup.groupId(); + // Get the offset information for the consumer group + ListConsumerGroupOffsetsResult consumerGroupOffsetsResult = adminClient.listConsumerGroupOffsets(groupId); + Map<TopicPartition, OffsetAndMetadata> topicOffsets = consumerGroupOffsetsResult.partitionsToOffsetAndMetadata().get(); + // Iterate over all TopicPartitions consumed by the consumer group + for (Map.Entry<TopicPartition, OffsetAndMetadata> entry : topicOffsets.entrySet()) { + String topic = entry.getKey().topic(); + topicConsumerGroupsMap.computeIfAbsent(topic, k -> new HashSet<>()).add(groupId); + } + } + return topicConsumerGroupsMap; + } + + private static Map<String, String> getConsumerGroupMetrics(String topic, String groupId, AdminClient adminClient) + throws ExecutionException, InterruptedException { + // Get the offset for each groupId for the specified topic + ListConsumerGroupOffsetsResult consumerGroupOffsetsResult = adminClient.listConsumerGroupOffsets(groupId); + Map<TopicPartition, OffsetAndMetadata> topicOffsets = consumerGroupOffsetsResult.partitionsToOffsetAndMetadata().get(); + long totalLag = 0L; + for (Entry<TopicPartition, OffsetAndMetadata> topicPartitionOffsetAndMetadataEntry : topicOffsets.entrySet()) { + if (topicPartitionOffsetAndMetadataEntry.getKey().topic().equals(topic)) { + OffsetAndMetadata offsetMetadata = topicPartitionOffsetAndMetadataEntry.getValue(); + TopicPartition partition = topicPartitionOffsetAndMetadataEntry.getKey(); + // Get the latest offset for each TopicPartition + ListOffsetsResultInfo resultInfo = adminClient.listOffsets( + Collections.singletonMap(partition, OffsetSpec.latest())).all().get().get(partition); + long latestOffset = resultInfo.offset(); + // Accumulate the lag for each partition + long l = latestOffset - offsetMetadata.offset(); + totalLag += l; + } + } + // Get all offsets and convert them to a string, joined by "、" + String partitionOffsets = topicOffsets.entrySet().stream() + .filter(entry -> entry.getKey().topic().equals(topic)) + .map(entry -> String.valueOf(entry.getValue().offset())) + .collect(Collectors.collectingAndThen( + Collectors.joining(","), + result -> "[" + result + "]" + )); + Map<String, String> res = new HashMap<>(); + res.put(LAG_NUM, String.valueOf(totalLag)); + res.put(PARTITION_OFFSET, partitionOffsets); + return res; + } + + private static boolean filterInternalTopics(String topic, Boolean monitorInternalTopic) { + if (monitorInternalTopic) { + return true; + } else { + return !InternalTopic.isInternalTopic(topic); + } + } + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + require(metrics != null, "Metrics cannot be null"); + KafkaProtocol kafkaProtocol = metrics.getKclient(); + + // Ensure that metrics and kafkaProtocol are not null + require(kafkaProtocol != null, "Kafka collect must have kafkaProtocol params"); + // Ensure that host and port are not empty + requireHasText(kafkaProtocol.getHost(), "Kafka Protocol host is required."); + requireHasText(kafkaProtocol.getPort(), "Kafka Protocol port is required."); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + try { + KafkaProtocol kafkaProtocol = metrics.getKclient(); + String command = kafkaProtocol.getCommand(); + Boolean monitorInternalTopic = Boolean.valueOf(kafkaProtocol.getMonitorInternalTopic()); + boolean isKafkaCommand = SupportedCommand.isKafkaCommand(command); + if (!isKafkaCommand) { + log.error("Unsupported command: {}", command); + builder.setCode(CollectRep.Code.FAIL); + return; + } + + // Create AdminClient with the provided host and port + AdminClient adminClient = getAdminClient(kafkaProtocol); + + // Execute the appropriate collection method based on the command + switch (SupportedCommand.fromCommand(command)) { + case TOPIC_DESCRIBE: + collectTopicDescribe(builder, adminClient, monitorInternalTopic); + break; + case TOPIC_LIST: + collectTopicList(builder, adminClient, monitorInternalTopic); + break; + case TOPIC_OFFSET: + collectTopicOffset(builder, adminClient, monitorInternalTopic); + break; + case CONSUMER_DETAIL: + collectTopicConsumerGroups(builder, adminClient, monitorInternalTopic); + break; + default: + log.error("Unsupported command: {}", command); + break; + } + } catch (InterruptedException | ExecutionException e) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Kafka collect error: " + e.getMessage()); + log.error("Kafka collect error", e); + } + } + + protected AdminClient getAdminClient(KafkaProtocol kafkaProtocol) { + CacheIdentifier kafkaAdminClientIdentifier = CacheIdentifier.builder() + .ip(kafkaProtocol.getHost()).port(kafkaProtocol.getPort()) + .build(); + Optional<AbstractConnection<?>> kafkaClientCache = + connectionCommonCache.getCache(kafkaAdminClientIdentifier, true); + if (kafkaClientCache.isPresent()) { + KafkaConnect kafkaConnect = (KafkaConnect) kafkaClientCache.get(); + AdminClient adminClient = kafkaConnect.getConnection(); + if (adminClient != null) { + return adminClient; + } + // Cached KafkaConnect is present but AdminClient is null, invalidate the cache + connectionCommonCache.removeCache(kafkaAdminClientIdentifier); + } + // Cached AdminClient was not present or was invalid; create a new AdminClient + Properties properties = new Properties(); + properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, + kafkaProtocol.getHost() + ":" + kafkaProtocol.getPort()); + AdminClient adminClient = KafkaAdminClient.create(properties); + if (adminClient == null) { + return null; + } + connectionCommonCache.addCache(kafkaAdminClientIdentifier, new KafkaConnect(adminClient)); + return adminClient; + } + + + /** + * Collect the earliest and latest offsets for each topic + * + * @param builder The MetricsData builder + * @param adminClient The AdminClient + * @throws InterruptedException If the thread is interrupted + * @throws ExecutionException If an error occurs during execution + */ + private void collectTopicOffset(CollectRep.MetricsData.Builder builder, AdminClient adminClient, + Boolean monitorInternalTopic) throws InterruptedException, ExecutionException { + ListTopicsResult listTopicsResult = adminClient.listTopics(new ListTopicsOptions().listInternal(true)); + Set<String> topicNames = listTopicsResult.names().get(); + topicNames.forEach(topicName -> { + if (filterInternalTopics(topicName, monitorInternalTopic)) { + try { + Map<String, TopicDescription> map = + adminClient.describeTopics(Collections.singleton(topicName)).all() + .get(3L, TimeUnit.SECONDS); + map.forEach((key, value) -> value.partitions() + .forEach(info -> extractedOffset(builder, adminClient, topicName, value, info))); + } catch (TimeoutException | InterruptedException | ExecutionException e) { + log.warn("Topic {} get offset fail", topicName); + } + } + }); + } + + private void extractedOffset(CollectRep.MetricsData.Builder builder, AdminClient adminClient, String name, + TopicDescription value, TopicPartitionInfo info) { + try { + TopicPartition topicPartition = new TopicPartition(value.name(), info.partition()); + long earliestOffset = getEarliestOffset(adminClient, topicPartition); + long latestOffset = getLatestOffset(adminClient, topicPartition); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + valueRowBuilder.addColumn(value.name()); + valueRowBuilder.addColumn(String.valueOf(info.partition())); + valueRowBuilder.addColumn(String.valueOf(earliestOffset)); + valueRowBuilder.addColumn(String.valueOf(latestOffset)); + builder.addValueRow(valueRowBuilder.build()); + } catch (TimeoutException | InterruptedException | ExecutionException e) { + log.warn("Topic {} get offset fail", name); + } + } + + /** + * Get the earliest offset for a given topic partition + * + * @param adminClient The AdminClient + * @param topicPartition The TopicPartition + * @return The earliest offset + */ + private long getEarliestOffset(AdminClient adminClient, TopicPartition topicPartition) + throws InterruptedException, ExecutionException, TimeoutException { + return adminClient + .listOffsets(Collections.singletonMap(topicPartition, OffsetSpec.earliest())) + .all() + .get(3L, TimeUnit.SECONDS) + .get(topicPartition) + .offset(); + } + + /** + * Get the latest offset for a given topic partition + * + * @param adminClient The AdminClient + * @param topicPartition The TopicPartition + * @return The latest offset + */ + private long getLatestOffset(AdminClient adminClient, TopicPartition topicPartition) + throws InterruptedException, ExecutionException, TimeoutException { + return adminClient + .listOffsets(Collections.singletonMap(topicPartition, OffsetSpec.latest())) + .all() + .get(3L, TimeUnit.SECONDS) + .get(topicPartition) + .offset(); + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_KAFKA; + } + + private static void require(boolean expression, String message) { + if (!expression) { + throw new IllegalArgumentException(message); + } + } + + private static void requireHasText(String value, String message) { + require(value != null && !value.trim().isEmpty(), message); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-kafka/src/main/java/org/apache/hertzbeat/collector/collect/kafka/KafkaConnect.java b/hertzbeat-collector/hertzbeat-collector-kafka/src/main/java/org/apache/hertzbeat/collector/collect/kafka/KafkaConnect.java new file mode 100644 index 0000000..671016d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-kafka/src/main/java/org/apache/hertzbeat/collector/collect/kafka/KafkaConnect.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.kafka; + +import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection; +import org.apache.kafka.clients.admin.AdminClient; + + +/** + * Kafka connection + */ +public class KafkaConnect extends AbstractConnection<AdminClient> { + private final AdminClient adminClient; + + public KafkaConnect(AdminClient adminClient) { + this.adminClient = adminClient; + } + + @Override + public AdminClient getConnection() { + return adminClient; + } + + @Override + public void closeConnection() { + if (adminClient != null) { + adminClient.close(); + } + } + +} \ No newline at end of file diff --git a/hertzbeat-collector/hertzbeat-collector-kafka/src/main/java/org/apache/hertzbeat/collector/collect/kafka/constants/InternalTopic.java b/hertzbeat-collector/hertzbeat-collector-kafka/src/main/java/org/apache/hertzbeat/collector/collect/kafka/constants/InternalTopic.java new file mode 100644 index 0000000..d97b08d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-kafka/src/main/java/org/apache/hertzbeat/collector/collect/kafka/constants/InternalTopic.java @@ -0,0 +1,61 @@ +/* + * 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.collector.collect.kafka.constants; + +import java.util.HashSet; +import java.util.Set; + +/** + * SupportedCommand + */ +public enum InternalTopic { + + CONSUMER_OFFSET("__consumer_offsets"); + + private static Set<String> INTERNAL_TOPIC = new HashSet<>(); + + static { + // O(1) complexity, using static to load all system placeholders + for (InternalTopic placeholder : InternalTopic.values()) { + INTERNAL_TOPIC.add(placeholder.getCommand()); + } + } + + private final String key; + + InternalTopic(String command) { + this.key = command; + } + + public String getCommand() { + return key; + } + + public static boolean isInternalTopic(String str) { + return INTERNAL_TOPIC.contains(str); + } + + public static InternalTopic fromCommand(String command) { + for (InternalTopic supportedCommand : InternalTopic.values()) { + if (supportedCommand.getCommand().equals(command)) { + return supportedCommand; + } + } + throw new IllegalArgumentException("No enum constant for command: " + command); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-kafka/src/main/java/org/apache/hertzbeat/collector/collect/kafka/constants/SupportedCommand.java b/hertzbeat-collector/hertzbeat-collector-kafka/src/main/java/org/apache/hertzbeat/collector/collect/kafka/constants/SupportedCommand.java new file mode 100644 index 0000000..685060c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-kafka/src/main/java/org/apache/hertzbeat/collector/collect/kafka/constants/SupportedCommand.java @@ -0,0 +1,64 @@ +/* + * 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.collector.collect.kafka.constants; + +import java.util.HashSet; +import java.util.Set; + +/** + * SupportedCommand + */ +public enum SupportedCommand { + + TOPIC_DESCRIBE("topic-describe"), + TOPIC_LIST("topic-list"), + TOPIC_OFFSET("topic-offset"), + CONSUMER_DETAIL("consumer-detail"); + + private static Set<String> SUPPORTED_COMMAND = new HashSet<>(); + + static { + // O(1) complexity, using static to load all system placeholders + for (SupportedCommand placeholder : SupportedCommand.values()) { + SUPPORTED_COMMAND.add(placeholder.getCommand()); + } + } + + private final String key; + + SupportedCommand(String command) { + this.key = command; + } + + public String getCommand() { + return key; + } + + public static boolean isKafkaCommand(String str) { + return SUPPORTED_COMMAND.contains(str); + } + + public static SupportedCommand fromCommand(String command) { + for (SupportedCommand supportedCommand : SupportedCommand.values()) { + if (supportedCommand.getCommand().equals(command)) { + return supportedCommand; + } + } + throw new IllegalArgumentException("No enum constant for command: " + command); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-kafka/src/test/java/org/apache/hertzbeat/collector/collect/kafka/KafkaCollectTest.java b/hertzbeat-collector/hertzbeat-collector-kafka/src/test/java/org/apache/hertzbeat/collector/collect/kafka/KafkaCollectTest.java new file mode 100644 index 0000000..a946ad5 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-kafka/src/test/java/org/apache/hertzbeat/collector/collect/kafka/KafkaCollectTest.java @@ -0,0 +1,138 @@ +/* + * 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.collector.collect.kafka; + +import org.apache.hertzbeat.collector.collect.kafka.constants.SupportedCommand; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.KafkaProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.ListTopicsOptions; +import org.apache.kafka.clients.admin.ListTopicsResult; +import org.apache.kafka.common.KafkaFuture; +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.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test case for {@link KafkaCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +public class KafkaCollectTest { + + private static final String HOST = "127.0.0.1"; + private static final String PORT = "9092"; + + private KafkaCollectImpl collect; + + @Mock + private AdminClient adminClient; + + @BeforeEach + public void setUp() throws Exception { + collect = new KafkaCollectImpl(){ + @Override + protected AdminClient getAdminClient(KafkaProtocol protocol) { + return adminClient; + } + }; + } + + @Test + void preCheck() { + // metrics is null + assertThrows(IllegalArgumentException.class, () -> { + collect.preCheck(null); + }); + + // kafka is null + assertThrows(IllegalArgumentException.class, () -> { + collect.preCheck(Metrics.builder().build()); + }); + + KafkaProtocol kafka = new KafkaProtocol(); + Metrics metric = Metrics.builder().kclient(kafka).build(); + // kafka srv host is null + assertThrows(IllegalArgumentException.class, () -> { + collect.preCheck(metric); + }); + // kafka port is null + assertThrows(IllegalArgumentException.class, () -> { + kafka.setHost(HOST); + collect.preCheck(metric); + }); + // no exception throw + assertDoesNotThrow(() -> { + kafka.setPort(PORT); + collect.preCheck(metric); + }); + } + + @Test + void testCollect() throws Exception { + Set<String> topicSet = new HashSet<>(); + topicSet.add("test-topic"); + + ListTopicsResult listTopicsResult = Mockito.mock(ListTopicsResult.class); + KafkaFuture<Set<String>> future = Mockito.mock(KafkaFuture.class); + + Mockito.when(adminClient.listTopics(Mockito.any(ListTopicsOptions.class))).thenReturn(listTopicsResult); + Mockito.when(listTopicsResult.names()).thenReturn(future); + Mockito.when(future.get()).thenReturn(topicSet); + + KafkaProtocol kafka = KafkaProtocol.builder() + .host(HOST) + .port(PORT) + .build(); + + Metrics metrics = Metrics.builder() + .kclient(kafka) + .build(); + + // test if not kafka command + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + collect.collect(builder, metrics); + assertEquals(CollectRep.Code.FAIL, builder.getCode()); + + //test kafka command + kafka.setCommand(SupportedCommand.TOPIC_LIST.getCommand()); + builder.setCode(CollectRep.Code.forNumber(6)); + collect.collect(builder, metrics); + + assertEquals(CollectRep.Code.SUCCESS, builder.getCode()); + assertEquals(1, builder.getValuesList().size()); + assertEquals("test-topic", builder.getValues(0).getColumns(0)); + Mockito.verify(adminClient).listTopics(Mockito.any(ListTopicsOptions.class)); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_KAFKA, collect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mongodb/pom.xml b/hertzbeat-collector/hertzbeat-collector-mongodb/pom.xml new file mode 100644 index 0000000..472dc55 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mongodb/pom.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-collector-mongodb</artifactId> + <name>${project.artifactId}</name> + + <properties> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-common</artifactId> + <scope>provided</scope> + </dependency> + <!-- mongodb --> + <dependency> + <groupId>org.mongodb</groupId> + <artifactId>mongodb-driver-sync</artifactId> + </dependency> + </dependencies> +</project> diff --git a/hertzbeat-collector/hertzbeat-collector-mongodb/src/main/java/org/apache/hertzbeat/collector/collect/mongodb/MongodbConnect.java b/hertzbeat-collector/hertzbeat-collector-mongodb/src/main/java/org/apache/hertzbeat/collector/collect/mongodb/MongodbConnect.java new file mode 100644 index 0000000..a5b0278 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mongodb/src/main/java/org/apache/hertzbeat/collector/collect/mongodb/MongodbConnect.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.mongodb; + +import com.mongodb.client.MongoClient; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection; + +/** + * mongodb connect client + */ +@Slf4j +public class MongodbConnect extends AbstractConnection<MongoClient> { + private final MongoClient mongoClient; + + public MongodbConnect(MongoClient mongoClient) { + this.mongoClient = mongoClient; + } + + @Override + public void closeConnection() throws Exception { + if (this.mongoClient != null) { + this.mongoClient.close(); + } + } + + @Override + public MongoClient getConnection() { + return mongoClient; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mongodb/src/main/java/org/apache/hertzbeat/collector/collect/mongodb/MongodbSingleCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-mongodb/src/main/java/org/apache/hertzbeat/collector/collect/mongodb/MongodbSingleCollectImpl.java new file mode 100644 index 0000000..0cf3107 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mongodb/src/main/java/org/apache/hertzbeat/collector/collect/mongodb/MongodbSingleCollectImpl.java @@ -0,0 +1,238 @@ +/* + * 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.collector.collect.mongodb; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoServerUnavailableException; +import com.mongodb.MongoTimeoutException; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection; +import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier; +import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.MongodbProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.bson.Document; + +/** + * Mongodb single collect + * see also https://www.mongodb.com/languages/java, + * https://www.mongodb.com/docs/manual/reference/command/serverStatus/#metrics + */ +@Slf4j +public class MongodbSingleCollectImpl extends AbstractCollect { + + /** + * mongodb diagnostic commands supported, excluding internal/deprecated related commands + * can refer to <a href="https://www.mongodb.com/docs/manual/reference/command/nav-diagnostic/">...</a>, + * <a href="https://www.mongodb.com/docs/mongodb-shell/run-commands/">...</a> + * Note: Some commands require the corresponding permissions to execute, + * otherwise the execution will not report an error, but the return result is empty. + * seeDetails <a href="https://www.mongodb.com/docs/manual/reference/built-in-roles/">...</a> + */ + private static final String[] SUPPORTED_MONGODB_DIAGNOSTIC_COMMANDS = { + "buildInfo", + "collStats", + "connPoolStats", + "connectionStatus", + "dbHash", + "dbStats", + "explain", + "features", + "getCmdLineOpts", + "getLog", + "hostInfo", + "listCommands", + "profile", + "serverStatus", + "top", + "validateDBMetadata", + }; + + private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance(); + + /** + * Check that the mongodb connection information in metrics is complete + */ + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException{ + require(metrics != null && metrics.getMongodb() != null, "Mongodb collect must has mongodb params"); + MongodbProtocol mongodbProtocol = metrics.getMongodb(); + requireHasText(mongodbProtocol.getCommand(), "Mongodb Protocol command is required."); + requireHasText(mongodbProtocol.getHost(), "Mongodb Protocol host is required."); + requireHasText(mongodbProtocol.getPort(), "Mongodb Protocol port is required."); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + // The command naming convention is the command supported by the above mongodb diagnostic. Support subdocument + // If the command does not include., execute the command directly and use the document it returns; + // otherwise, you need to execute the metricsParts[0] command first and then obtain the related subdocument + // For example serverStatus. Metrics. Operation support serverStatus command the metrics of the following document operation documents + String[] metricsParts = metrics.getMongodb().getCommand().split("\\."); + String command = metricsParts[0]; + // Check whether the first part of the. Split is a command supported by the mongodb diagnostic + if (Arrays.stream(SUPPORTED_MONGODB_DIAGNOSTIC_COMMANDS).noneMatch(command::equals)) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("unsupported mongodb diagnostic command: " + command); + return; + } + MongoClient mongoClient; + CacheIdentifier identifier = null; + try { + identifier = getIdentifier(metrics.getMongodb()); + mongoClient = getClient(metrics, identifier); + MongoDatabase mongoDatabase = mongoClient.getDatabase(metrics.getMongodb().getDatabase()); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + Document document = mongoDatabase.runCommand(new Document(command, 1)); + for (int i = 1; i < metricsParts.length; i++) { + document = (Document) document.get(metricsParts[i]); + } + if (document == null) { + throw new RuntimeException("the document get from command " + metrics.getMongodb().getCommand() + " is null."); + } + fillBuilder(metrics, valueRowBuilder, document); + builder.addValueRow(valueRowBuilder.build()); + } catch (MongoServerUnavailableException | MongoTimeoutException unavailableException) { + connectionCommonCache.removeCache(identifier); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + String message = CommonUtil.getMessageFromThrowable(unavailableException); + builder.setMsg(message); + } catch (Exception e) { + builder.setCode(CollectRep.Code.FAIL); + String message = CommonUtil.getMessageFromThrowable(e); + builder.setMsg(message); + log.warn(message, e); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_MONGODB; + } + + /** + * Populate the valueRowBuilder with the collection metrics configured in Metrics and the documentation returned by executing the mongodb command + */ + private void fillBuilder(Metrics metrics, CollectRep.ValueRow.Builder valueRowBuilder, Document document) { + metrics.getAliasFields().forEach(it -> { + if (document.containsKey(it)) { + Object fieldValue = document.get(it); + if (fieldValue == null) { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } else { + valueRowBuilder.addColumn(fieldValue.toString()); + } + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + }); + } + + public static CacheIdentifier getIdentifier(MongodbProtocol mongodbProtocol){ + // try to reuse connection + return CacheIdentifier.builder() + .ip(mongodbProtocol.getHost()).port(mongodbProtocol.getPort()) + .username(mongodbProtocol.getUsername()).password(mongodbProtocol.getPassword()).build(); + } + + /** + * Obtained from mongodb connection information in metrics + * The mongodb client itself does not have network calls and network links. + * For each collection, we need to create a new session and close it after use + * mongodb client is thread pool, we need to create the session for each collect + */ + private MongoClient getClient(Metrics metrics, CacheIdentifier identifier) { + MongodbProtocol mongodbProtocol = metrics.getMongodb(); + + Optional<AbstractConnection<?>> cacheOption = connectionCommonCache.getCache(identifier, true); + MongoClient mongoClient = null; + if (cacheOption.isPresent()) { + MongodbConnect mongodbConnect = (MongodbConnect) cacheOption.get(); + mongoClient = mongodbConnect.getConnection(); + } + if (mongoClient != null) { + return mongoClient; + } + + String url = null; + if (CollectorConstants.MONGO_DB_ATLAS_MODEL.equals(mongodbProtocol.getModel())) { + if (StringUtils.isBlank(mongodbProtocol.getUsername()) && StringUtils.isBlank(mongodbProtocol.getPassword())) { + // Anonymous access for MongoDB Atlas + url = String.format("mongodb+srv://%s/%s", mongodbProtocol.getHost(), mongodbProtocol.getDatabase()); + } else { + url = String.format("mongodb+srv://%s:%s@%s/%s?authSource=%s", mongodbProtocol.getUsername(), + URLEncoder.encode(mongodbProtocol.getPassword(), StandardCharsets.UTF_8), mongodbProtocol.getHost(), + mongodbProtocol.getDatabase(), mongodbProtocol.getAuthenticationDatabase()); + } + } else { + // If the multiplexing fails, create a new connection to connect to mongodb + // Passwords may contain special characters and need to be encoded using JS-like encodeURIComponent, which uses java URLEncoder + if (StringUtils.isBlank(mongodbProtocol.getUsername()) && StringUtils.isBlank(mongodbProtocol.getPassword())) { + // Anonymous access for standalone MongoDB + url = String.format("mongodb://%s:%s/%s", mongodbProtocol.getHost(), mongodbProtocol.getPort(), + mongodbProtocol.getDatabase()); + } else { + url = String.format("mongodb://%s:%s@%s:%s/%s?authSource=%s", mongodbProtocol.getUsername(), + URLEncoder.encode(mongodbProtocol.getPassword(), StandardCharsets.UTF_8), mongodbProtocol.getHost(), mongodbProtocol.getPort(), + mongodbProtocol.getDatabase(), mongodbProtocol.getAuthenticationDatabase()); + } + } + + + // Use the Mongo Client Settings builder to configure timeouts and other configurations + MongoClientSettings settings = MongoClientSettings.builder() + .applyConnectionString(new ConnectionString(url)) + .applyToClusterSettings(builder -> + // Set the timeout period for server selection + builder.serverSelectionTimeout(Long.parseLong(mongodbProtocol.getTimeout()), MILLISECONDS)) + .build(); + + // CREATE THE MONGO CLIENT USING THE CONFIGURATION + mongoClient = MongoClients.create(settings); + + MongodbConnect mongodbConnect = new MongodbConnect(mongoClient); + connectionCommonCache.addCache(identifier, mongodbConnect, 3600 * 1000L); + return mongoClient; + } + + private static void require(boolean expression, String message) { + if (!expression) { + throw new IllegalArgumentException(message); + } + } + + private static void requireHasText(String value, String message) { + require(value != null && !value.trim().isEmpty(), message); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mongodb/src/test/java/org/apache/hertzbeat/collector/collect/mongodb/MongoCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-mongodb/src/test/java/org/apache/hertzbeat/collector/collect/mongodb/MongoCollectImplTest.java new file mode 100644 index 0000000..2e275bc --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mongodb/src/test/java/org/apache/hertzbeat/collector/collect/mongodb/MongoCollectImplTest.java @@ -0,0 +1,108 @@ +/* + * 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.collector.collect.mongodb; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.MongodbProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.bson.Document; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + + +/** + * Test case for {@link MongodbSingleCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +public class MongoCollectImplTest { + + @Mock + MongodbProtocol mongodbProtocol; + @Mock + MongoClient mongoClient; + + @Mock + MongoDatabase mongoDatabase; + + @InjectMocks + MongodbSingleCollectImpl mongodbSingleCollect; + + @BeforeEach + void setUp() { + mongodbProtocol = MongodbProtocol.builder() + .host("127.0.0.1") + .port("5000") + .username("Administrator") + .password("Password") + .timeout("5000") + .database("test") + .authenticationDatabase("admin") + .build(); + } + + @Test + void mockTest() { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + mongodbProtocol.setCommand("hostInfo.os"); + Metrics metrics = new Metrics(); + metrics.setAliasFields(List.of("type", "name", "version")); + metrics.setMongodb(mongodbProtocol); + String osInfo = """ + { + "os" : + { + "type" : "Linux", + "name" : "Ubuntu", + "version" : "22.04" + } + }"""; + Mockito.when(mongoDatabase.runCommand(new Document("hostInfo", 1))).thenReturn(Document.parse(osInfo)); + Mockito.when(mongoClient.getDatabase("test")).thenReturn(mongoDatabase); + MockedStatic<MongoClients> mongoClientsMockedStatic = Mockito.mockStatic(MongoClients.class); + String url = String.format("mongodb://%s:%s@%s:%s/%s?authSource=%s", mongodbProtocol.getUsername(), + URLEncoder.encode(mongodbProtocol.getPassword(), StandardCharsets.UTF_8), mongodbProtocol.getHost(), mongodbProtocol.getPort(), + mongodbProtocol.getDatabase(), mongodbProtocol.getAuthenticationDatabase()); + MongoClientSettings settings = MongoClientSettings.builder() + .applyConnectionString(new ConnectionString(url)) + .applyToClusterSettings(b -> + b.serverSelectionTimeout(Long.parseLong(mongodbProtocol.getTimeout()), MILLISECONDS)) + .build(); + mongoClientsMockedStatic.when(() -> MongoClients.create(settings)).thenReturn(mongoClient); + mongodbSingleCollect.preCheck(metrics); + mongodbSingleCollect.collect(builder, metrics); + Assertions.assertEquals("Linux", builder.getValues(0).getColumns(0)); + Assertions.assertEquals("Ubuntu", builder.getValues(0).getColumns(1)); + Assertions.assertEquals("22.04", builder.getValues(0).getColumns(2)); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/pom.xml b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/pom.xml new file mode 100644 index 0000000..6c631b1 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/pom.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-collector-mysql-r2dbc</artifactId> + <name>${project.artifactId}</name> + + <properties> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>io.asyncer</groupId> + <artifactId>r2dbc-mysql</artifactId> + <version>${r2dbc-mysql.version}</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-autoconfigure</artifactId> + </dependency> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers</artifactId> + <version>${testcontainers.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers-junit-jupiter</artifactId> + <version>${testcontainers.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-tcnative-boringssl-static</artifactId> + <version>2.0.69.Final</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlQueryExecutor.java b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlQueryExecutor.java new file mode 100644 index 0000000..0c47603 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlQueryExecutor.java @@ -0,0 +1,33 @@ +/* + * 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.collector.mysql.r2dbc; + +/** + * Internal collector-side MySQL query executor. + */ +public interface MysqlQueryExecutor { + + /** + * Execute a single read-only SQL statement. + * + * @param sql SQL to execute + * @param options execution options and target connection settings + * @return normalized query result + */ + QueryResult execute(String sql, QueryOptions options); +} diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlR2dbcConfiguration.java b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlR2dbcConfiguration.java new file mode 100644 index 0000000..fcce478 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlR2dbcConfiguration.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.mysql.r2dbc; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Spring beans for the collector-side MySQL R2DBC route. + */ +@Configuration(proxyBeanMethods = false) +public class MysqlR2dbcConfiguration { + + @Bean + @ConditionalOnMissingBean + public SqlGuard mysqlR2dbcSqlGuard() { + return new SqlGuard(); + } + + @Bean + @ConditionalOnMissingBean + public ResultSetMapper mysqlR2dbcResultSetMapper() { + return new ResultSetMapper(); + } + + @Bean + @ConditionalOnMissingBean + public MysqlR2dbcConnectionFactoryProvider mysqlR2dbcConnectionFactoryProvider() { + return new MysqlR2dbcConnectionFactoryProvider(); + } + + @Bean + @ConditionalOnMissingBean(MysqlQueryExecutor.class) + public MysqlQueryExecutor mysqlQueryExecutor(MysqlR2dbcConnectionFactoryProvider connectionFactoryProvider, + ResultSetMapper resultSetMapper, + SqlGuard sqlGuard) { + return new MysqlR2dbcQueryExecutor(connectionFactoryProvider, resultSetMapper, sqlGuard); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlR2dbcConnectionFactoryProvider.java b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlR2dbcConnectionFactoryProvider.java new file mode 100644 index 0000000..3121538 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlR2dbcConnectionFactoryProvider.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.mysql.r2dbc; + +import io.asyncer.r2dbc.mysql.MySqlConnectionConfiguration; +import io.asyncer.r2dbc.mysql.MySqlConnectionFactory; +import io.asyncer.r2dbc.mysql.constant.SslMode; +import io.asyncer.r2dbc.mysql.constant.TlsVersions; +import org.springframework.util.StringUtils; + +/** + * Builds R2DBC MySQL connection factories for the collector. + */ +public class MysqlR2dbcConnectionFactoryProvider { + + /** + * Create a connection factory for a target MySQL instance. + * + * @param options target connection settings + * @return connection factory + */ + public MySqlConnectionFactory create(QueryOptions options) { + return create(options, SslMode.PREFERRED); + } + + public MySqlConnectionFactory create(QueryOptions options, SslMode sslMode) { + if (!StringUtils.hasText(options.getHost())) { + throw new IllegalArgumentException("R2DBC MySQL collector route requires a target host"); + } + MySqlConnectionConfiguration.Builder builder = MySqlConnectionConfiguration.builder() + .host(options.getHost()) + .port(options.getPort()) + .connectTimeout(options.getTimeout()) + .sslMode(sslMode) + .tcpKeepAlive(true) + .tcpNoDelay(true); + if (sslMode.startSsl()) { + // Pin to TLSv1.2 because JDK 25 + the current server matrix is unreliable when TLSv1.3 is negotiated. + builder.tlsVersion(TlsVersions.TLS1_2); + } + if (StringUtils.hasText(options.getUsername())) { + builder.user(options.getUsername()); + } + if (options.getPassword() != null) { + builder.password(options.getPassword()); + } + if (StringUtils.hasText(options.resolvedDatabase())) { + builder.database(options.resolvedDatabase()); + } + return MySqlConnectionFactory.from(builder.build()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlR2dbcQueryExecutor.java b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlR2dbcQueryExecutor.java new file mode 100644 index 0000000..d8a5e96 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlR2dbcQueryExecutor.java @@ -0,0 +1,156 @@ +/* + * 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.collector.mysql.r2dbc; + +import io.asyncer.r2dbc.mysql.MySqlConnectionFactory; +import io.asyncer.r2dbc.mysql.constant.SslMode; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.Statement; +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.TimeoutException; +import reactor.core.publisher.Mono; + +/** + * Collector-side MySQL query executor backed by R2DBC. + */ +public class MysqlR2dbcQueryExecutor implements MysqlQueryExecutor { + + private final MysqlR2dbcConnectionFactoryProvider connectionFactoryProvider; + private final ResultSetMapper resultSetMapper; + private final SqlGuard sqlGuard; + + public MysqlR2dbcQueryExecutor(MysqlR2dbcConnectionFactoryProvider connectionFactoryProvider, + ResultSetMapper resultSetMapper, + SqlGuard sqlGuard) { + this.connectionFactoryProvider = connectionFactoryProvider; + this.resultSetMapper = resultSetMapper; + this.sqlGuard = sqlGuard; + } + + @Override + public QueryResult execute(String sql, QueryOptions options) { + String normalizedSql = sqlGuard.normalizeAndValidate(sql); + QueryResult firstAttempt = executeOnce(normalizedSql, options, SslMode.PREFERRED); + if (!firstAttempt.hasError() || !shouldRetryWithoutSsl(firstAttempt.getError())) { + return firstAttempt; + } + QueryResult fallbackAttempt = executeOnce(normalizedSql, options, SslMode.DISABLED); + if (!fallbackAttempt.hasError()) { + return fallbackAttempt; + } + if (requiresSslCompatibleAuth(fallbackAttempt.getError())) { + return QueryResult.builder() + .error(fallbackAttempt.getError() + + ". This route currently needs a TLS-compatible runtime or a mysql_native_password monitoring user.") + .build(); + } + return fallbackAttempt; + } + + private QueryResult executeOnce(String sql, QueryOptions options, SslMode sslMode) { + MySqlConnectionFactory connectionFactory = connectionFactoryProvider.create(options, sslMode); + Duration timeout = options.getTimeout().plusSeconds(1); + Connection connection = null; + try { + connection = Mono.from(connectionFactory.create()).block(timeout); + if (connection == null) { + return QueryResult.builder().error("R2DBC MySQL collector route could not create a connection").build(); + } + QueryResult result = map(connection, sql, options).block(timeout); + if (result == null) { + return QueryResult.builder().error("R2DBC MySQL collector route returned no result").build(); + } + return result; + } catch (IllegalArgumentException exception) { + throw exception; + } catch (Exception exception) { + String message = extractErrorMessage(exception, options.getTimeout()); + return QueryResult.builder() + .error(message) + .build(); + } finally { + if (connection != null) { + safelyClose(connection); + } + } + } + + private void safelyClose(Connection connection) { + try { + Mono.from(connection.close()) + .timeout(Duration.ofSeconds(1), Mono.empty()) + .onErrorResume(error -> Mono.empty()) + .block(Duration.ofSeconds(2)); + } catch (Exception ignored) { + // Best-effort close only. Query completion must not be turned into a failure by cleanup. + } + } + + private String extractErrorMessage(Exception exception, Duration timeout) { + if (isTimeoutException(exception)) { + return "Query timed out after " + timeout.toMillis() + "ms"; + } + String message = exception.getMessage(); + if ((message == null || message.isBlank()) && exception.getCause() != null) { + message = exception.getCause().getMessage(); + } + return message == null || message.isBlank() ? exception.getClass().getSimpleName() : message; + } + + private boolean isTimeoutException(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof TimeoutException) { + return true; + } + String message = current.getMessage(); + if (message != null && message.toLowerCase(Locale.ROOT).contains("timeout on blocking read")) { + return true; + } + current = current.getCause(); + } + return false; + } + + private boolean shouldRetryWithoutSsl(String error) { + if (error == null) { + return false; + } + String normalized = error.toLowerCase(Locale.ROOT); + return normalized.contains("handshake_failure") + || normalized.contains("ssl/tls handshake") + || normalized.contains("closedchannelexception") + || normalized.contains("connection unexpectedly closed"); + } + + private boolean requiresSslCompatibleAuth(String error) { + if (error == null) { + return false; + } + String normalized = error.toLowerCase(Locale.ROOT); + return normalized.contains("caching_sha2_password") + || normalized.contains("must require ssl"); + } + + private Mono<QueryResult> map(Connection connection, String sql, QueryOptions options) { + Statement statement = connection.createStatement(sql); + // Keep the query path read-only and deterministic, and let JdbcCommonCollect own the existing parser flow. + return resultSetMapper.map(statement, options.getTimeout(), options.getMaxRows()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/QueryOptions.java b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/QueryOptions.java new file mode 100644 index 0000000..ffad2d2 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/QueryOptions.java @@ -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.collector.mysql.r2dbc; + +import java.time.Duration; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import org.springframework.util.StringUtils; + +/** + * Collector-only execution options for a target MySQL query. + */ +@Getter +@Builder(toBuilder = true) +@ToString(exclude = "password") +public class QueryOptions { + + private final String host; + @Builder.Default + private final int port = 3306; + private final String username; + private final String password; + private final String database; + private final String schema; + @Builder.Default + private final Duration timeout = Duration.ofSeconds(6); + @Builder.Default + private final int maxRows = 1000; + @Builder.Default + private final int fetchSize = 256; + @Builder.Default + private final boolean readOnly = true; + + public String resolvedDatabase() { + if (StringUtils.hasText(database)) { + return database; + } + if (StringUtils.hasText(schema)) { + return schema; + } + return null; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/QueryResult.java b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/QueryResult.java new file mode 100644 index 0000000..93c5e58 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/QueryResult.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.mysql.r2dbc; + +import java.util.Collections; +import java.util.List; + +import lombok.Builder; +import lombok.Getter; + +/** + * Normalized query response. + */ +@Getter +@Builder(toBuilder = true) +public class QueryResult { + + @Builder.Default + private final List<String> columns = Collections.emptyList(); + @Builder.Default + private final List<List<String>> rows = Collections.emptyList(); + @Builder.Default + private final long elapsedMs = 0L; + @Builder.Default + private final int rowCount = 0; + private final String error; + + public boolean hasError() { + return error != null && !error.isBlank(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/ResultSetMapper.java b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/ResultSetMapper.java new file mode 100644 index 0000000..5ad8971 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/ResultSetMapper.java @@ -0,0 +1,95 @@ +/* + * 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.collector.mysql.r2dbc; + +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; +import io.r2dbc.spi.Statement; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Maps R2DBC results into the collector-neutral result model. + */ +public class ResultSetMapper { + + /** + * Execute and map a single statement. + * + * @param statement statement to execute + * @param timeout timeout for the query + * @param maxRows max rows to materialize + * @return normalized query result + */ + public Mono<QueryResult> map(Statement statement, Duration timeout, int maxRows) { + long startNanos = System.nanoTime(); + ResultAccumulator accumulator = new ResultAccumulator(); + return Flux.from(statement.execute()) + .concatMap(result -> Flux.from(result.map(accumulator::mapRow))) + .take(maxRows) + .collectList() + .map(rows -> accumulator.toQueryResult(rows, elapsedMillis(startNanos))) + .timeout(timeout); + } + + private static long elapsedMillis(long startNanos) { + return Duration.ofNanos(System.nanoTime() - startNanos).toMillis(); + } + + private static ColumnLayout extractColumns(RowMetadata metadata) { + List<String> columns = new ArrayList<>(); + metadata.getColumnMetadatas().forEach(columnMetadata -> columns.add(columnMetadata.getName())); + return new ColumnLayout(List.copyOf(columns), columns.size()); + } + + private static List<String> extractRow(Row row, int columnCount) { + List<String> values = new ArrayList<>(columnCount); + for (int index = 0; index < columnCount; index++) { + Object value = row.get(index); + values.add(value == null ? null : String.valueOf(value)); + } + return values; + } + + private record ColumnLayout(List<String> columns, int columnCount) { + } + + private static final class ResultAccumulator { + + private ColumnLayout columnLayout; + + private List<String> mapRow(Row row, RowMetadata metadata) { + if (columnLayout == null) { + columnLayout = extractColumns(metadata); + } + return extractRow(row, columnLayout.columnCount()); + } + + private QueryResult toQueryResult(List<List<String>> rows, long elapsedMs) { + return QueryResult.builder() + .columns(columnLayout == null ? List.of() : columnLayout.columns()) + .rows(rows) + .rowCount(rows.size()) + .elapsedMs(elapsedMs) + .build(); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/SqlGuard.java b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/SqlGuard.java new file mode 100644 index 0000000..47cb257 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/main/java/org/apache/hertzbeat/collector/mysql/r2dbc/SqlGuard.java @@ -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.collector.mysql.r2dbc; + +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Minimal SQL guard for the built-in read-only MySQL collector route. + */ +public class SqlGuard { + + private static final Pattern TRAILING_SEMICOLONS = Pattern.compile(";\\s*$"); + private static final Pattern COMMENTS = Pattern.compile("(/\\*|\\*/|--|#)"); + private static final Pattern FORBIDDEN = Pattern.compile( + "\\b(insert|update|delete|replace|merge|alter|drop|truncate|create|call)\\b"); + + /** + * Normalize a single read-only SQL statement and reject obvious unsafe statements. + * + * @param sql sql text + * @return normalized sql text + */ + public String normalizeAndValidate(String sql) { + if (sql == null || sql.isBlank()) { + throw new IllegalArgumentException("R2DBC MySQL collector route requires a non-empty SQL statement"); + } + String normalized = TRAILING_SEMICOLONS.matcher(sql.trim()).replaceAll("").trim(); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("R2DBC MySQL collector route requires a non-empty SQL statement"); + } + if (normalized.indexOf(';') >= 0) { + throw new IllegalArgumentException("R2DBC MySQL collector route only allows a single SQL statement"); + } + if (COMMENTS.matcher(normalized).find()) { + throw new IllegalArgumentException("R2DBC MySQL collector route does not allow SQL comments"); + } + + String lower = normalized.toLowerCase(Locale.ROOT); + if (!(lower.startsWith("select") || lower.startsWith("show"))) { + throw new IllegalArgumentException("R2DBC MySQL collector route only supports SELECT or SHOW statements"); + } + + String stripped = stripQuotedContent(lower); + if (FORBIDDEN.matcher(stripped).find()) { + throw new IllegalArgumentException("R2DBC MySQL collector route only supports read-only statements"); + } + return normalized; + } + + private String stripQuotedContent(String sql) { + StringBuilder builder = new StringBuilder(sql.length()); + char quote = 0; + for (int i = 0; i < sql.length(); i++) { + char current = sql.charAt(i); + if (quote == 0 && (current == '\'' || current == '"' || current == '`')) { + quote = current; + builder.append(' '); + continue; + } + if (quote != 0 && current == quote) { + quote = 0; + builder.append(' '); + continue; + } + builder.append(quote == 0 ? current : ' '); + } + return builder.toString(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/test/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlR2dbcQueryExecutorIntegrationTest.java b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/test/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlR2dbcQueryExecutorIntegrationTest.java new file mode 100644 index 0000000..532c433 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/test/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlR2dbcQueryExecutorIntegrationTest.java @@ -0,0 +1,201 @@ +/* + * 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.collector.mysql.r2dbc; + +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.time.Duration; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +class MysqlR2dbcQueryExecutorIntegrationTest { + + private static final String TEST_DATABASE = "hzb"; + private static final String TEST_USERNAME = "test"; + private static final String TEST_PASSWORD = "test123"; + + private final MysqlQueryExecutor executor = new MysqlR2dbcQueryExecutor( + new MysqlR2dbcConnectionFactoryProvider(), new ResultSetMapper(), new SqlGuard()); + + @TestFactory + Stream<DynamicTest> shouldRunReadOnlyQueriesAcrossCompatibilityMatrix() { + return Stream.of( + new DatabaseTarget("mysql-5.7", DockerImageName.parse("mysql:5.7.44"), false, false), + new DatabaseTarget("mysql-8.0-default-auth", DockerImageName.parse("mysql:8.0.36"), false, false), + new DatabaseTarget("mysql-8.0-native-auth", DockerImageName.parse("mysql:8.0.36"), false, true), + new DatabaseTarget("mariadb-11.4", DockerImageName.parse("mariadb:11.4"), true, false)) + .map(target -> DynamicTest.dynamicTest(target.name(), () -> verifyReadOnlyQueries(target))); + } + + @Test + void shouldRejectIllegalSqlBeforeConnecting() { + assertThrows(IllegalArgumentException.class, () -> executor.execute("DELETE FROM sample_metrics", QueryOptions.builder() + .host("127.0.0.1") + .port(3306) + .username("test") + .password("test123") + .database("hzb") + .build())); + } + + @Test + void shouldReturnTimeoutErrorOnSlowQuery() throws Exception { + Assumptions.assumeTrue(DockerClientFactory.instance().isDockerAvailable(), "Docker is required for integration tests"); + DatabaseTarget target = new DatabaseTarget("mysql-8.0-native-auth", DockerImageName.parse("mysql:8.0.36"), false, true); + try (GenericContainer<?> container = createContainer(target)) { + container.start(); + awaitTcpLoginReady(container); + QueryResult result = executor.execute("SELECT SLEEP(3)", buildOptions(container, TEST_DATABASE, Duration.ofSeconds(1))); + assertTrue(result.hasError()); + } + } + + @Test + void shouldSupportMysql8DefaultCachingSha2Users() throws Exception { + Assumptions.assumeTrue(DockerClientFactory.instance().isDockerAvailable(), "Docker is required for integration tests"); + DatabaseTarget target = new DatabaseTarget("mysql-8.0-default-auth", DockerImageName.parse("mysql:8.0.36"), false, false); + try (GenericContainer<?> container = createContainer(target)) { + container.start(); + awaitTcpLoginReady(container); + initSchema(container); + QueryResult result = executor.execute("SELECT 1 AS value", buildOptions(container, TEST_DATABASE, Duration.ofSeconds(5))); + assertFalse(result.hasError(), () -> "mysql-8.0-default-auth failed: " + result.getError()); + assertEquals(List.of("value"), result.getColumns()); + assertEquals(List.of(List.of("1")), result.getRows()); + } + } + + private void verifyReadOnlyQueries(DatabaseTarget target) throws Exception { + Assumptions.assumeTrue(DockerClientFactory.instance().isDockerAvailable(), "Docker is required for integration tests"); + try (GenericContainer<?> container = createContainer(target)) { + container.start(); + awaitTcpLoginReady(container); + initSchema(container); + + QueryOptions options = buildOptions(container, TEST_DATABASE, Duration.ofSeconds(5)); + + QueryResult selectResult = executor.execute("SELECT 1 AS value", options); + assertFalse(selectResult.hasError(), + () -> target.name() + " SELECT 1 failed: " + selectResult.getError()); + assertEquals(List.of("value"), selectResult.getColumns()); + assertEquals(List.of(List.of("1")), selectResult.getRows()); + + QueryResult showResult = executor.execute("SHOW VARIABLES LIKE 'version%'", options); + assertFalse(showResult.hasError(), + () -> target.name() + " SHOW VARIABLES failed: " + showResult.getError()); + assertTrue(showResult.getRowCount() > 0); + + QueryResult businessResult = executor.execute("SELECT label FROM sample_metrics WHERE id = 1", options); + assertFalse(businessResult.hasError(), + () -> target.name() + " business SQL failed: " + businessResult.getError()); + assertEquals(List.of(List.of("alpha")), businessResult.getRows()); + } + } + + private GenericContainer<?> createContainer(DatabaseTarget target) { + GenericContainer<?> container = new GenericContainer<>(target.image()) + .withExposedPorts(3306) + .waitingFor(Wait.forListeningPort()); + if (target.mariaDb()) { + container.withEnv("MARIADB_DATABASE", TEST_DATABASE) + .withEnv("MARIADB_USER", TEST_USERNAME) + .withEnv("MARIADB_PASSWORD", TEST_PASSWORD) + .withEnv("MARIADB_ROOT_PASSWORD", "root123"); + return container; + } + container.withEnv("MYSQL_DATABASE", TEST_DATABASE) + .withEnv("MYSQL_USER", TEST_USERNAME) + .withEnv("MYSQL_PASSWORD", TEST_PASSWORD) + .withEnv("MYSQL_ROOT_PASSWORD", "root123"); + if (target.mysqlNativePasswordUser()) { + container.withCommand("--default-authentication-plugin=mysql_native_password"); + } + return container; + } + + private QueryOptions buildOptions(GenericContainer<?> container, String database, Duration timeout) { + return QueryOptions.builder() + .host(normalizeLoopbackHost(container.getHost())) + .port(container.getMappedPort(3306)) + .username(TEST_USERNAME) + .password(TEST_PASSWORD) + .database(database) + .timeout(timeout) + .maxRows(1000) + .fetchSize(128) + .readOnly(true) + .build(); + } + + private void awaitTcpLoginReady(GenericContainer<?> container) throws Exception { + long deadline = System.currentTimeMillis() + Duration.ofSeconds(30).toMillis(); + String command = String.join(" ", + "CLIENT=$(command -v mysql || command -v mariadb)", + "&&", + "$CLIENT --protocol=TCP -h127.0.0.1 -P3306", + "-u" + TEST_USERNAME, + "-p" + TEST_PASSWORD, + TEST_DATABASE, + "-e", + "\"SELECT 1\""); + while (System.currentTimeMillis() < deadline) { + try { + var result = container.execInContainer("sh", "-lc", command); + if (result.getExitCode() == 0) { + return; + } + } catch (Exception ignored) { + // The entrypoint may still be switching from the temporary bootstrap server to the final one. + } + Thread.sleep(1000); + } + throw new IllegalStateException("Timed out waiting for MySQL TCP login to become ready"); + } + + private String normalizeLoopbackHost(String host) { + return host; + } + + private void initSchema(GenericContainer<?> container) throws Exception { + String command = String.join(" ", + "CLIENT=$(command -v mysql || command -v mariadb)", + "&&", + "$CLIENT --protocol=TCP -h127.0.0.1 -P3306", + "-u" + TEST_USERNAME, + "-p" + TEST_PASSWORD, + TEST_DATABASE, + "-e", + "\"CREATE TABLE IF NOT EXISTS sample_metrics (id INT PRIMARY KEY, label VARCHAR(32));", + "REPLACE INTO sample_metrics (id, label) VALUES (1, 'alpha');\""); + container.execInContainer("sh", "-lc", command); + } + + private record DatabaseTarget(String name, DockerImageName image, boolean mariaDb, boolean mysqlNativePasswordUser) { + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/test/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlSqlTemplateCompatibilityTest.java b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/test/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlSqlTemplateCompatibilityTest.java new file mode 100644 index 0000000..687cf06 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/test/java/org/apache/hertzbeat/collector/mysql/r2dbc/MysqlSqlTemplateCompatibilityTest.java @@ -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.collector.mysql.r2dbc; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; + +class MysqlSqlTemplateCompatibilityTest { + + private final SqlGuard sqlGuard = new SqlGuard(); + + @Test + @SuppressWarnings("unchecked") + void shouldAcceptCurrentOfficialMysqlTemplateSql() throws IOException { + Path template = Path.of("..", "..", "hertzbeat-manager", "src", "main", "resources", "define", "app-mysql.yml") + .toAbsolutePath() + .normalize(); + assertTrue(Files.isRegularFile(template), "MySQL monitor template must exist"); + + Yaml yaml = new Yaml(); + int sqlCount = 0; + try (Reader reader = Files.newBufferedReader(template)) { + Map<String, Object> root = yaml.load(reader); + List<Map<String, Object>> metrics = (List<Map<String, Object>>) root.get("metrics"); + for (Map<String, Object> metric : metrics) { + Map<String, Object> jdbc = (Map<String, Object>) metric.get("jdbc"); + if (jdbc == null) { + continue; + } + Object sql = jdbc.get("sql"); + if (!(sql instanceof String sqlText)) { + continue; + } + String normalized = sqlGuard.normalizeAndValidate(sqlText); + assertFalse(normalized.isBlank(), "Normalized SQL should not be blank"); + sqlCount++; + } + } + assertTrue(sqlCount > 0, "MySQL monitor template should contain SQL statements"); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/test/java/org/apache/hertzbeat/collector/mysql/r2dbc/ResultSetMapperTest.java b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/test/java/org/apache/hertzbeat/collector/mysql/r2dbc/ResultSetMapperTest.java new file mode 100644 index 0000000..2ec085b --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/test/java/org/apache/hertzbeat/collector/mysql/r2dbc/ResultSetMapperTest.java @@ -0,0 +1,81 @@ +/* + * 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.collector.mysql.r2dbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.r2dbc.spi.ColumnMetadata; +import io.r2dbc.spi.Result; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; +import io.r2dbc.spi.Statement; +import java.time.Duration; +import java.util.List; +import java.util.function.BiFunction; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +class ResultSetMapperTest { + + private final ResultSetMapper mapper = new ResultSetMapper(); + + @Test + @SuppressWarnings("unchecked") + void shouldCacheColumnLayoutAcrossRows() { + Statement statement = mock(Statement.class); + Result result = mock(Result.class); + RowMetadata metadata = mock(RowMetadata.class); + Row firstRow = mock(Row.class); + Row secondRow = mock(Row.class); + ColumnMetadata firstColumn = mock(ColumnMetadata.class); + ColumnMetadata secondColumn = mock(ColumnMetadata.class); + + when(firstColumn.getName()).thenReturn("id"); + when(secondColumn.getName()).thenReturn("label"); + doReturn(List.of(firstColumn, secondColumn)).when(metadata).getColumnMetadatas(); + when(firstRow.get(0)).thenReturn(1); + when(firstRow.get(1)).thenReturn("alpha"); + when(secondRow.get(0)).thenReturn(2); + when(secondRow.get(1)).thenReturn("beta"); + when(result.map(any(BiFunction.class))).thenAnswer(invocation -> { + BiFunction<Row, RowMetadata, List<String>> mapping = + (BiFunction<Row, RowMetadata, List<String>>) invocation.getArgument(0); + return Flux.just( + mapping.apply(firstRow, metadata), + mapping.apply(secondRow, metadata)); + }); + doReturn(Flux.just(result)).when(statement).execute(); + + QueryResult queryResult = mapper.map(statement, Duration.ofSeconds(1), 10).block(); + + assertNotNull(queryResult); + assertEquals(List.of("id", "label"), queryResult.getColumns()); + assertEquals(List.of( + List.of("1", "alpha"), + List.of("2", "beta")), queryResult.getRows()); + assertEquals(2, queryResult.getRowCount()); + verify(metadata, times(1)).getColumnMetadatas(); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/test/java/org/apache/hertzbeat/collector/mysql/r2dbc/SqlGuardTest.java b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/test/java/org/apache/hertzbeat/collector/mysql/r2dbc/SqlGuardTest.java new file mode 100644 index 0000000..b46235c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-mysql-r2dbc/src/test/java/org/apache/hertzbeat/collector/mysql/r2dbc/SqlGuardTest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.mysql.r2dbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class SqlGuardTest { + + private final SqlGuard sqlGuard = new SqlGuard(); + + @Test + void shouldNormalizeTrailingSemicolon() { + assertEquals("SELECT 1", sqlGuard.normalizeAndValidate("SELECT 1;")); + } + + @Test + void shouldAllowShowStatement() { + assertEquals("SHOW VARIABLES LIKE 'version%'", sqlGuard.normalizeAndValidate("SHOW VARIABLES LIKE 'version%'")); + } + + @Test + void shouldRejectWriteStatement() { + assertThrows(IllegalArgumentException.class, () -> sqlGuard.normalizeAndValidate("DELETE FROM test")); + } + + @Test + void shouldRejectMultipleStatements() { + assertThrows(IllegalArgumentException.class, () -> sqlGuard.normalizeAndValidate("SELECT 1; SELECT 2")); + } + + @Test + void shouldRejectComments() { + assertThrows(IllegalArgumentException.class, () -> sqlGuard.normalizeAndValidate("SELECT 1 -- comment")); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-nebulagraph/pom.xml b/hertzbeat-collector/hertzbeat-collector-nebulagraph/pom.xml new file mode 100644 index 0000000..9ea2e9d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-nebulagraph/pom.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-collector-nebulagraph</artifactId> + <name>${project.artifactId}</name> + + <properties> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-common</artifactId> + <scope>provided</scope> + </dependency> + <!-- nebula graph client --> + <dependency> + <groupId>com.vesoft</groupId> + <artifactId>client</artifactId> + <version>${vesoft-client.version}</version> + <exclusions> + <exclusion> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + </exclusion> + <exclusion> + <groupId>com.alibaba</groupId> + <artifactId>fastjson</artifactId> + </exclusion> + </exclusions> + </dependency> + </dependencies> +</project> diff --git a/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/main/java/org/apache/hertzbeat/collector/collect/nebulagraph/NebulaGraphCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/main/java/org/apache/hertzbeat/collector/collect/nebulagraph/NebulaGraphCollectImpl.java new file mode 100644 index 0000000..6b2777b --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/main/java/org/apache/hertzbeat/collector/collect/nebulagraph/NebulaGraphCollectImpl.java @@ -0,0 +1,209 @@ +/* + * 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.collector.collect.nebulagraph; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.NebulaGraphProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; + +/** + * NebulaGraph collect + */ +@Slf4j +public class NebulaGraphCollectImpl extends AbstractCollect { + private static final int SUCCESS_CODE = 200; + + private static final String[] TIME_RANGE = new String[]{"5", "60", "600", "3600"}; + + private static final String REGEX = "\\.%s\\="; + + private static final String STR_SPLIT = "\n"; + + private static final String STORAGE_SPLIT_KEY_VALUE = "="; + + private static final String GRAPH_API = "/stats"; + + private static final String STORAGE_API = "/rocksdb_stats"; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getNebulaGraph() == null) { + throw new IllegalArgumentException("NebulaGraph collect must has NebulaGraph params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + long startTime = System.currentTimeMillis(); + NebulaGraphProtocol nebulaGraph = metrics.getNebulaGraph(); + String timePeriod = nebulaGraph.getTimePeriod(); + + if (!Objects.isNull(nebulaGraph.getTimePeriod()) && !Arrays.asList(TIME_RANGE).contains(timePeriod)) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("The time range for metric statistics, currently supporting 5 seconds, 60 seconds, 600 seconds, and 3600 seconds."); + return; + } + + if (nebulaGraph.getHost() == null || nebulaGraph.getHost().isEmpty()) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("The host of NebulaGraph must be set"); + return; + } + + String resp; + long responseTime; + HashMap<String, String> resultMap = new HashMap<>(64); + HttpContext httpContext = createHttpContext(nebulaGraph.getHost(), nebulaGraph.getPort()); + HttpUriRequest request = createHttpRequest(nebulaGraph.getHost(), nebulaGraph.getPort(), + nebulaGraph.getUrl(), nebulaGraph.getTimeout()); + // Send an HTTP request to obtain response data + try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != SUCCESS_CODE) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("StatusCode " + statusCode); + return; + } + resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + responseTime = System.currentTimeMillis() - startTime; + resultMap.put(CollectorConstants.RESPONSE_TIME, Long.toString(responseTime)); + // Parse the response differently depending on the API + if (GRAPH_API.equals(nebulaGraph.getUrl())) { + parseStatsResponse(resp, nebulaGraph.getTimePeriod(), resultMap); + } else if (STORAGE_API.equals(nebulaGraph.getUrl())) { + parseStorageResponse(resp, resultMap); + } + List<String> aliasFields = metrics.getAliasFields(); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String field : aliasFields) { + String fieldValue = resultMap.get(field); + valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } + builder.addValueRow(valueRowBuilder.build()); + } catch (IOException e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.info(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_NEBULAGRAPH; + } + + private HttpContext createHttpContext(String host, String port) { + HttpHost httpHost = new HttpHost(host, Integer.parseInt(port)); + HttpClientContext httpClientContext = new HttpClientContext(); + httpClientContext.setTargetHost(httpHost); + return httpClientContext; + } + + private HttpUriRequest createHttpRequest(String host, String port, String url, String timeoutStr) { + RequestBuilder requestBuilder = RequestBuilder.get(); + // uri + String uri = CollectUtil.replaceUriSpecialChar(url); + if (IpDomainUtil.isHasSchema(host)) { + requestBuilder.setUri(host + ":" + port + uri); + } else { + String ipAddressType = IpDomainUtil.checkIpAddressType(host); + String baseUri = CollectorConstants.IPV6.equals(ipAddressType) + ? String.format("[%s]:%s", host, port + uri) + : String.format("%s:%s", host, port + uri); + + requestBuilder.setUri(CollectorConstants.HTTP_HEADER + baseUri); + } + + requestBuilder.addHeader(HttpHeaders.CONNECTION, "keep-alive"); + requestBuilder.addHeader(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36"); + + requestBuilder.addHeader(HttpHeaders.ACCEPT, "text/plain"); + + int timeout = Integer.parseInt(timeoutStr); + if (timeout > 0) { + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(timeout) + .setSocketTimeout(timeout) + .setRedirectsEnabled(true) + .build(); + requestBuilder.setConfig(requestConfig); + } + return requestBuilder.build(); + } + + /** + * Parse Stats response and filter by time period + * + * @param responseBody response body + * @param timePeriod time period + */ + private void parseStatsResponse(String responseBody, String timePeriod, HashMap<String, String> resultMap) { + // Set up regular expression matching + String timeRegex = String.format(REGEX, timePeriod); + Pattern pattern = Pattern.compile(timeRegex); + String[] strArray = responseBody.split(STR_SPLIT); + for (String str : strArray) { + Matcher matcher = pattern.matcher(str); + if (matcher.find()) { + String[] split = str.split(timeRegex); + resultMap.put(split[0], split[1]); + } + } + } + + + /** + * Parse the Storage response and filter by time period + * + * @param responseBody response body + */ + private void parseStorageResponse(String responseBody, HashMap<String, String> resultMap) { + String[] strArray = responseBody.split(STR_SPLIT); + for (String str : strArray) { + String[] split = str.split(STORAGE_SPLIT_KEY_VALUE); + resultMap.put(split[0], split[1]); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/main/java/org/apache/hertzbeat/collector/collect/nebulagraph/NebulaTemplate.java b/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/main/java/org/apache/hertzbeat/collector/collect/nebulagraph/NebulaTemplate.java new file mode 100644 index 0000000..fb7cfcc --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/main/java/org/apache/hertzbeat/collector/collect/nebulagraph/NebulaTemplate.java @@ -0,0 +1,157 @@ +/* + * 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.collector.collect.nebulagraph; + +import com.vesoft.nebula.client.graph.NebulaPoolConfig; +import com.vesoft.nebula.client.graph.data.HostAddress; +import com.vesoft.nebula.client.graph.data.ResultSet; +import com.vesoft.nebula.client.graph.data.ValueWrapper; +import com.vesoft.nebula.client.graph.exception.IOErrorException; +import com.vesoft.nebula.client.graph.net.NebulaPool; +import com.vesoft.nebula.client.graph.net.Session; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.entity.job.protocol.NgqlProtocol; + +/** + * template for connect nebula graph and execute ngql + */ +@Slf4j +public class NebulaTemplate { + + private String spaceName; + private Session session; + NebulaPool pool; + + public void closeSessionAndPool() { + if (session != null && session.ping()) { + session.close(); + } + if (pool != null) { + pool.close(); + } + } + + @SneakyThrows + public boolean initSession(NgqlProtocol protocol) { + HostAddress hostAddress = new HostAddress(protocol.getHost(), Integer.parseInt(protocol.getPort())); + this.spaceName = protocol.getSpaceName(); + pool = new NebulaPool(); + + NebulaPoolConfig nebulaPoolConfig = new NebulaPoolConfig(); + nebulaPoolConfig.setMaxConnSize(100); + nebulaPoolConfig.setTimeout(Integer.parseInt(protocol.getTimeout())); + boolean initResult = pool + .init(Collections.singletonList(hostAddress), nebulaPoolConfig); + if (!initResult) { + log.error("pool init failed."); + return false; + } + session = pool.getSession(protocol.getUsername(), protocol.getPassword(), false); + return true; + } + + private ResultSet execute(String ngql) { + ResultSet resultSet; + try { + resultSet = session.execute(ngql); + return resultSet; + } catch (IOErrorException e) { + log.error("Query error:【{}】,ErrorMsg:【{}】", ngql, e.getMessage()); + session.close(); + } + return null; + } + + /** + * Execute ngql query using the graph space name configured in the protocol + * + * @param ngql ngql + */ + public List<Map<String, Object>> executeCommand(String ngql) { + return executeCommand(ngql, spaceName); + } + + public List<Map<String, Object>> executeCommand(String ngql, String currentSpaceName) { + if (StringUtils.isNotBlank(currentSpaceName)) { + ngql = "USE " + currentSpaceName + ";" + ngql; + } + ResultSet resultSet = execute(ngql); + if (resultSet == null) { + log.error("Query Error result set is null,NGQL:【{}】", ngql); + return Collections.emptyList(); + } + List<String> columnNames = resultSet.getColumnNames(); + List<Map<String, Object>> result = new ArrayList<>(); + int size = resultSet.getRows().size(); + for (int i = 0; i < size; i++) { + result.add(new LinkedHashMap<>()); + } + for (String columnName : columnNames) { + List<ValueWrapper> valueWrappers = resultSet.colValues(columnName); + for (int i = 0; i < valueWrappers.size(); i++) { + result.get(i).put(columnName, convert(valueWrappers.get(i))); + } + } + return result; + + } + + + @SneakyThrows + private Object convert(ValueWrapper wrapper) { + if (wrapper.isBoolean()) { + return wrapper.asBoolean(); + } + if (wrapper.isString()) { + return wrapper.asString(); + } + if (wrapper.isDate()) { + return wrapper.asDate(); + } + if (wrapper.isDateTime()) { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSS") + .parse(wrapper.asDateTime().getLocalDateTimeStr().replace("T", " ")); + } + if (wrapper.isLong()) { + return wrapper.asLong(); + } + if (wrapper.isDouble()) { + return wrapper.asDouble(); + } + if (wrapper.isPath()) { + return wrapper.asPath().toString(); + } + if (wrapper.isEdge()) { + return wrapper.asRelationship().toString(); + } + if (wrapper.isVertex()) { + return wrapper.asNode().toString(); + } + return null; + } + + +} diff --git a/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/main/java/org/apache/hertzbeat/collector/collect/nebulagraph/NgqlCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/main/java/org/apache/hertzbeat/collector/collect/nebulagraph/NgqlCollectImpl.java new file mode 100644 index 0000000..4034c1e --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/main/java/org/apache/hertzbeat/collector/collect/nebulagraph/NgqlCollectImpl.java @@ -0,0 +1,253 @@ +/* + * 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.collector.collect.nebulagraph; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.constants.CollectorConstants; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.NgqlProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.entity.message.CollectRep.MetricsData.Builder; + +/** + * connect nebulaGraph and collect metrics use NGQL + */ +public class NgqlCollectImpl extends AbstractCollect { + + private static final String PARSE_TYPE_FILTER_COUNT = "filterCount"; + private static final String PARSE_TYPE_ONE_ROW = "oneRow"; + private static final String PARSE_TYPE_MULTI_ROW = "multiRow"; + private static final String PARSE_TYPE_COLUMNS = "columns"; + private static final String STATUS_RUNNING = "RUNNING"; + private static final String STATUS_QUEUE = "QUEUE"; + + private static final String COMMAND_SHOW_JOBS = "SHOW JOBS;"; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + NgqlProtocol ngql = metrics.getNgql(); + requireHasText(ngql.getHost(), "NGQL protocol host is required"); + requireHasText(ngql.getPort(), "Port protocol host is required"); + requireHasText(ngql.getParseType(), "NGQL protocol parseType is required"); + requireHasText(ngql.getUsername(), "NGQL protocol username is required"); + requireHasText(ngql.getPassword(), "NGQL protocol password is required"); + } + + @Override + public void collect(Builder builder, Metrics metrics) { + NgqlProtocol ngql = metrics.getNgql(); + long startTimeNanos = System.nanoTime(); + NebulaTemplate nebulaTemplate = new NebulaTemplate(); + try { + boolean initSuccess = nebulaTemplate.initSession(ngql); + if (!initSuccess) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Failed to connect Nebula Graph"); + return; + } + } catch (Exception e) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(e.getMessage()); + return; + } + + long responseTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos); + try { + switch (ngql.getParseType()) { + case PARSE_TYPE_FILTER_COUNT -> filterCount(nebulaTemplate, ngql, metrics.getAliasFields(), builder, responseTime); + case PARSE_TYPE_ONE_ROW -> queryOneRow(nebulaTemplate, ngql, metrics.getAliasFields(), builder, responseTime); + case PARSE_TYPE_MULTI_ROW -> queryMultiRow(nebulaTemplate, ngql.getCommands(), metrics.getAliasFields(), builder, responseTime); + case PARSE_TYPE_COLUMNS -> queryColumns(nebulaTemplate, ngql.getCommands(), metrics.getAliasFields(), builder, responseTime); + default -> { + } + } + } finally { + nebulaTemplate.closeSessionAndPool(); + } + } + + /** + * parseType filterCount Filter the result set according to requirements and count the quantity + * command: field#ngql#filterName#filterValue + * + * @param nebulaTemplate template for execute ngql + * @param protocol ngql protocol + * @param columns metrics aliasField + * @param responseTime cost time for connect to nebula graph + */ + private void filterCount(NebulaTemplate nebulaTemplate, NgqlProtocol protocol, List<String> columns, CollectRep.MetricsData.Builder builder, Long responseTime) { + Map<String, String> data = new HashMap<>(); + for (String command : protocol.getCommands()) { + Map<String, String> showJobs = showJobs(nebulaTemplate, protocol.getSpaceName(), command); + if (Objects.nonNull(showJobs)) { + data.putAll(showJobs); + continue; + } + + String[] parts = command.split("#", -1); + if (parts.length != 4) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("The command:[" + command + "] does not meet the requirements"); + } + String fieldName = parts[0]; + String ngql = parts[1]; + String filterName = parts[2]; + String filterValue = parts[3]; + try { + List<Map<String, Object>> maps = nebulaTemplate.executeCommand(ngql); + Stream<Map<String, Object>> stream = maps.stream(); + if (StringUtils.isNotBlank(filterName)) { + stream = stream.filter(map -> Objects.equals(map.get(filterName), filterValue)); + } + long count = stream.count(); + data.put(fieldName, Long.toString(count)); + } catch (Exception e) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Query error:[" + ngql + "],Msg:[" + e.getMessage() + "]"); + } + + } + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String column : columns) { + if (CollectorConstants.RESPONSE_TIME.equals(column)) { + valueRowBuilder.addColumn(String.valueOf(responseTime)); + } else { + String value = data.get(column); + value = value == null ? CommonConstants.NULL_VALUE : value; + valueRowBuilder.addColumn(value); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + + private void queryOneRow(NebulaTemplate nebulaTemplate, NgqlProtocol protocol, List<String> columns, CollectRep.MetricsData.Builder builder, Long responseTime) { + Map<String, Object> queryResult = new HashMap<>(); + for (String command : protocol.getCommands()) { + Map<String, String> showJobs = showJobs(nebulaTemplate, protocol.getSpaceName(), command); + if (Objects.nonNull(showJobs)) { + queryResult.putAll(showJobs); + continue; + } + List<Map<String, Object>> maps = nebulaTemplate.executeCommand(command); + if (!maps.isEmpty()) { + queryResult.putAll(maps.get(0)); + } + } + inflateData(columns, responseTime, queryResult, builder); + } + + private void queryMultiRow(NebulaTemplate nebulaTemplate, List<String> commands, List<String> columns, CollectRep.MetricsData.Builder builder, Long responseTime) { + String command = commands.get(0); + try { + List<Map<String, Object>> result = nebulaTemplate.executeCommand(command); + for (Map<String, Object> row : result) { + inflateData(columns, responseTime, row, builder); + } + } catch (Exception e) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Query error:[" + command + "],Msg:[" + e.getMessage() + "]"); + } + + } + + private void queryColumns(NebulaTemplate nebulaTemplate, List<String> commands, List<String> columns, CollectRep.MetricsData.Builder builder, Long responseTime) { + Map<String, Object> resultMap = new HashMap<>(); + for (String command : commands) { + try { + List<Map<String, Object>> result = nebulaTemplate.executeCommand(command); + if (!result.isEmpty() && result.get(0).size() < 2) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Parsing type columns requires the result set to contain at least two columns"); + return; + } + for (Map<String, Object> map : result) { + List<Entry<String, Object>> data = map.entrySet().stream().limit(2).toList(); + resultMap.put(Objects.toString(data.get(0).getValue()), data.get(1).getValue()); + } + } catch (Exception e) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Query error:[" + command + "],Msg:[" + e.getMessage() + "]"); + } + } + inflateData(columns, responseTime, resultMap, builder); + } + + private void inflateData(List<String> columns, Long responseTime, Map<String, Object> dataFromDb, CollectRep.MetricsData.Builder builder) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String column : columns) { + if (CollectorConstants.RESPONSE_TIME.equals(column)) { + valueRowBuilder.addColumn(String.valueOf(responseTime)); + } else { + Object value = dataFromDb.get(column); + value = value == null ? CommonConstants.NULL_VALUE : value; + valueRowBuilder.addColumn(Objects.toString(value)); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_NGQL; + } + + private Map<String, String> showJobs(NebulaTemplate template, String protocolSpaceName, String command) { + if (!command.equalsIgnoreCase(COMMAND_SHOW_JOBS)) { + return null; + } + List<String> spaceNames = new ArrayList<>(); + Map<String, String> result = new HashMap<>(); + List<Map<String, Object>> jobs = new ArrayList<>(); + if (StringUtils.isNotBlank(protocolSpaceName)) { + spaceNames.add(protocolSpaceName); + } else { + List<Map<String, Object>> spaces = template.executeCommand("SHOW SPACES;"); + if (spaces.isEmpty()) { + return result; + } + List<String> allSpace = spaces.stream().map(s -> s.get("Name")) + .filter(Objects::nonNull) + .map(Objects::toString) + .toList(); + spaceNames.addAll(allSpace); + } + for (String name : spaceNames) { + jobs.addAll(template.executeCommand(COMMAND_SHOW_JOBS, name)); + } + result.put("queue_jobs", String.valueOf(jobs.stream().filter(job -> Objects.equals(job.get("Status"), STATUS_QUEUE)).count())); + result.put("running_jobs", String.valueOf(jobs.stream().filter(job -> Objects.equals(job.get("Status"), STATUS_RUNNING)).count())); + return result; + } + + private static void requireHasText(String value, String message) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException(message); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/test/java/org/apache/hertzbeat/collector/collect/nebulagraph/NebulaGraphCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/test/java/org/apache/hertzbeat/collector/collect/nebulagraph/NebulaGraphCollectImplTest.java new file mode 100644 index 0000000..6793f34 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/test/java/org/apache/hertzbeat/collector/collect/nebulagraph/NebulaGraphCollectImplTest.java @@ -0,0 +1,104 @@ +/* + * 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.collector.collect.nebulagraph; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.NebulaGraphProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link NebulaGraphCollectImpl} + */ +public class NebulaGraphCollectImplTest { + private NebulaGraphCollectImpl nebulaGraphCollect; + private CollectRep.MetricsData.Builder builder; + + @BeforeEach + void setUp() { + nebulaGraphCollect = new NebulaGraphCollectImpl(); + builder = CollectRep.MetricsData.newBuilder(); + } + + @Test + void preCheck() { + // metrics is null + assertThrows(IllegalArgumentException.class, () -> { + nebulaGraphCollect.preCheck(null); + }); + + // nebulaGraph is null + assertThrows(IllegalArgumentException.class, () -> { + Metrics metrics = Metrics.builder().build(); + nebulaGraphCollect.preCheck(metrics); + }); + + // everything is ok + assertDoesNotThrow(() -> { + Metrics metrics = Metrics.builder().nebulaGraph(NebulaGraphProtocol.builder().build()).build(); + nebulaGraphCollect.preCheck(metrics); + }); + } + + @Test + void collect() { + String[] validTimePeriods = new String[]{"5", "60", "600", "3600"}; + + // valid time period without host + for (String validTimePeriod : validTimePeriods) { + builder = CollectRep.MetricsData.newBuilder(); + Metrics metrics = Metrics.builder().nebulaGraph(NebulaGraphProtocol.builder().timePeriod(validTimePeriod).build()).build(); + nebulaGraphCollect.collect(builder, metrics); + Assertions.assertEquals(CollectRep.Code.FAIL, builder.getCode()); + } + + // invalid time period + assertDoesNotThrow(() -> { + builder = CollectRep.MetricsData.newBuilder(); + Metrics metrics = Metrics.builder().nebulaGraph(NebulaGraphProtocol.builder().timePeriod("invalid").build()).build(); + nebulaGraphCollect.collect(builder, metrics); + Assertions.assertEquals(CollectRep.Code.FAIL, builder.getCode()); + }); + + assertDoesNotThrow(() -> { + // valid time period and host + builder = CollectRep.MetricsData.newBuilder(); + Metrics metrics = Metrics.builder().nebulaGraph(NebulaGraphProtocol.builder() + .timePeriod("5") + .host("localhost") + .port("9090") + .url("example.com") + .timeout("1") + .build()).build(); + nebulaGraphCollect.collect(builder, metrics); + Assertions.assertEquals(CollectRep.Code.FAIL, builder.getCode()); + }); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_NEBULAGRAPH, nebulaGraphCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/test/java/org/apache/hertzbeat/collector/collect/nebulagraph/NgqlCollectImplTest.java b/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/test/java/org/apache/hertzbeat/collector/collect/nebulagraph/NgqlCollectImplTest.java new file mode 100644 index 0000000..4ba760d --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-nebulagraph/src/test/java/org/apache/hertzbeat/collector/collect/nebulagraph/NgqlCollectImplTest.java @@ -0,0 +1,208 @@ +/* + * 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.collector.collect.nebulagraph; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.NgqlProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link NgqlCollectImpl} + */ +@ExtendWith(MockitoExtension.class) +class NgqlCollectImplTest { + + @InjectMocks + private NgqlCollectImpl ngqlCollect; + + private NgqlProtocol ngqlProtocol; + + @BeforeEach + public void init() { + ngqlProtocol = NgqlProtocol.builder() + .host("127.0.0.1") + .port("9669") + .password("123456") + .username("root") + .timeout("60000").build(); + } + + @Test + void testOneRowCollect() { + String ngql = "SHOW COLLATION;"; + String charset = "utf8"; + String collation = "utf8_bin"; + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + ngqlProtocol.setCommands(Collections.singletonList(ngql)); + ngqlProtocol.setParseType("oneRow"); + List<String> aliasField = Arrays.asList("Collation", "Charset"); + + List<Map<String, Object>> result = new ArrayList<>(); + Map<String, Object> data = new HashMap<>(); + data.put("Collation", "utf8_bin"); + data.put("Charset", "utf8"); + result.add(data); + + MockedConstruction<NebulaTemplate> mocked = + Mockito.mockConstruction(NebulaTemplate.class, (template, context) -> { + Mockito.doNothing().when(template).closeSessionAndPool(); + Mockito.when(template.initSession(ngqlProtocol)).thenReturn(true); + Mockito.when(template.executeCommand(ngql)).thenReturn(result); + }); + + Metrics metrics = new Metrics(); + metrics.setNgql(ngqlProtocol); + metrics.setAliasFields(aliasField); + ngqlCollect.preCheck(metrics); + ngqlCollect.collect(builder, metrics); + Assertions.assertEquals(builder.getValuesCount(), 1); + Assertions.assertEquals(builder.getValues(0).getColumns(0), collation); + Assertions.assertEquals(builder.getValues(0).getColumns(1), charset); + mocked.close(); + } + + @Test + void testFilterCountCollect() { + String command = "offline#SHOW HOSTS#Status#OFFLINE"; + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + ngqlProtocol.setCommands(Collections.singletonList(command)); + ngqlProtocol.setParseType("filterCount"); + List<String> aliasField = Collections.singletonList("offline"); + + List<Map<String, Object>> result = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + Map<String, Object> data = new HashMap<>(); + data.put("Host", "graph" + 0); + data.put("Port", "9669"); + data.put("Status", i == 0 ? "OFFLINE" : "ONLINE"); + result.add(data); + } + MockedConstruction<NebulaTemplate> mocked = + Mockito.mockConstruction(NebulaTemplate.class, (template, context) -> { + Mockito.doNothing().when(template).closeSessionAndPool(); + Mockito.when(template.initSession(ngqlProtocol)).thenReturn(true); + Mockito.when(template.executeCommand("SHOW HOSTS")).thenReturn(result); + }); + + Metrics metrics = new Metrics(); + metrics.setNgql(ngqlProtocol); + metrics.setAliasFields(aliasField); + ngqlCollect.preCheck(metrics); + ngqlCollect.collect(builder, metrics); + Assertions.assertEquals(1, builder.getValuesCount()); + Assertions.assertEquals("1", builder.getValues(0).getColumns(0)); + mocked.close(); + } + + @Test + void testMultiRowCollect() { + String command = "SHOW HOSTS"; + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + ngqlProtocol.setCommands(Collections.singletonList(command)); + ngqlProtocol.setParseType("multiRow"); + List<String> aliasField = Arrays.asList("Host", "Port", "Status"); + + List<Map<String, Object>> result = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + Map<String, Object> data = new LinkedHashMap<>(); + data.put("Host", "graph" + i); + data.put("Port", "9669"); + data.put("Status", i == 0 ? "OFFLINE" : "ONLINE"); + result.add(data); + } + MockedConstruction<NebulaTemplate> mocked = + Mockito.mockConstruction(NebulaTemplate.class, (template, context) -> { + Mockito.doNothing().when(template).closeSessionAndPool(); + Mockito.when(template.initSession(ngqlProtocol)).thenReturn(true); + Mockito.when(template.executeCommand(command)).thenReturn(result); + }); + + Metrics metrics = new Metrics(); + metrics.setNgql(ngqlProtocol); + metrics.setAliasFields(aliasField); + ngqlCollect.preCheck(metrics); + ngqlCollect.collect(builder, metrics); + Assertions.assertEquals(3, builder.getValuesCount()); + for (int i = 0; i < result.size(); i++) { + List<Map.Entry<String, Object>> list = new ArrayList<>(result.get(i).entrySet()); + for (int j = 0; j < list.size(); j++) { + Assertions.assertEquals(list.get(j).getValue().toString(), builder.getValues(i).getColumns(j)); + } + } + mocked.close(); + } + + @Test + void testColumnsCollect() { + String command = "SHOW HOSTS"; + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + ngqlProtocol.setCommands(Collections.singletonList(command)); + ngqlProtocol.setParseType("columns"); + List<String> aliasField = Arrays.asList("graph0", "graph1", "graph2"); + + List<Map<String, Object>> result = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + Map<String, Object> data = new LinkedHashMap<>(); + data.put("Host", "graph" + i); + data.put("Port", "9669" + i); + data.put("Status", i == 0 ? "OFFLINE" : "ONLINE"); + result.add(data); + } + MockedConstruction<NebulaTemplate> mocked = + Mockito.mockConstruction(NebulaTemplate.class, (template, context) -> { + Mockito.doNothing().when(template).closeSessionAndPool(); + Mockito.when(template.initSession(ngqlProtocol)).thenReturn(true); + Mockito.when(template.executeCommand(command)).thenReturn(result); + }); + + Metrics metrics = new Metrics(); + metrics.setNgql(ngqlProtocol); + metrics.setAliasFields(aliasField); + ngqlCollect.preCheck(metrics); + ngqlCollect.collect(builder, metrics); + Assertions.assertEquals(1, builder.getValuesCount()); + for (int i = 0; i < 3; i++) { + Assertions.assertEquals("9669" + i, builder.getValues(0).getColumns(i)); + } + mocked.close(); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_NGQL, ngqlCollect.supportProtocol()); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-rocketmq/pom.xml b/hertzbeat-collector/hertzbeat-collector-rocketmq/pom.xml new file mode 100644 index 0000000..92d0501 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-rocketmq/pom.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-collector-rocketmq</artifactId> + <name>${project.artifactId}</name> + + <dependencies> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-common</artifactId> + <scope>provided</scope> + </dependency> + <!-- rocketmq --> + <dependency> + <groupId>org.apache.rocketmq</groupId> + <artifactId>rocketmq-tools</artifactId> + </dependency> + </dependencies> + +</project> diff --git a/hertzbeat-collector/hertzbeat-collector-rocketmq/src/main/java/org/apache/hertzbeat/collector/collect/rocketmq/RocketmqCollectData.java b/hertzbeat-collector/hertzbeat-collector-rocketmq/src/main/java/org/apache/hertzbeat/collector/collect/rocketmq/RocketmqCollectData.java new file mode 100644 index 0000000..57053f4 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-rocketmq/src/main/java/org/apache/hertzbeat/collector/collect/rocketmq/RocketmqCollectData.java @@ -0,0 +1,166 @@ +/* + * 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.collector.collect.rocketmq; + +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * rocketmq collect data + */ +@Data +public class RocketmqCollectData { + + /** + * cluster broker info + */ + private List<ClusterBrokerData> clusterBrokerDataList; + + /** + * consumer info + */ + private List<ConsumerInfo> consumerInfoList; + + /** + * topic info + * Map[key: TopicName, value: Topic Queue info List] + */ + private List<Map<String, List<TopicQueueInfo>>> topicInfoList; + + /** + * ClusterBrokerData + */ + @Data + public static class ClusterBrokerData { + + /** + * broker id + */ + private Long brokerId; + + /** + * broker address + */ + private String address; + + /** + * mq version + */ + private String version; + + /** + * producer send message tps + */ + private double producerMessageTps; + + /** + * consumer receive message tps + */ + private double consumerMessageTps; + + /** + * yesterday producer send message count + */ + private long yesterdayProduceCount; + + /** + * today producer send message count + */ + private long todayProduceCount; + + /** + * yesterday consumer receive message count + */ + private long yesterdayConsumeCount; + + /** + * today consumer receive message count + */ + private long todayConsumeCount; + } + + /** + * ConsumerInfo + */ + @Data + public static class ConsumerInfo { + + /** + * consumer group + */ + private String consumerGroup; + + /** + * client num + */ + private int clientQuantity; + + /** + * message model + */ + private String messageModel; + + /** + * consume type + */ + private String consumeType; + + /** + * consume tps + */ + private double consumeTps; + + /** + * message delay + */ + private long diffTotal; + } + + /** + * TopicQueueInfo + */ + @Data + public static class TopicQueueInfo { + + /** + * broker name + */ + private String brokerName; + + /** + * queue id + */ + private int queueId; + + /** + * message queue min offset + */ + private long minOffset; + + /** + * message queue max offset + */ + private long maxOffset; + + /** + * last update time(ms) + */ + private long lastUpdateTimestamp; + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-rocketmq/src/main/java/org/apache/hertzbeat/collector/collect/rocketmq/RocketmqSingleCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-rocketmq/src/main/java/org/apache/hertzbeat/collector/collect/rocketmq/RocketmqSingleCollectImpl.java new file mode 100644 index 0000000..a98ff1a --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-rocketmq/src/main/java/org/apache/hertzbeat/collector/collect/rocketmq/RocketmqSingleCollectImpl.java @@ -0,0 +1,384 @@ +/* + * 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.collector.collect.rocketmq; + +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Lists; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.JsonPathParser; +import org.apache.hertzbeat.common.concurrent.ManagedExecutor; +import org.apache.hertzbeat.common.concurrent.ManagedExecutors; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.RocketmqProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.rocketmq.acl.common.AclClientRPCHook; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.admin.ConsumeStats; +import org.apache.rocketmq.common.protocol.body.ClusterInfo; +import org.apache.rocketmq.common.protocol.body.ConsumerConnection; +import org.apache.rocketmq.common.protocol.body.KVTable; +import org.apache.rocketmq.common.protocol.body.SubscriptionGroupWrapper; +import org.apache.rocketmq.common.protocol.body.TopicList; +import org.apache.rocketmq.common.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; + +/** + * rocketmq collect + */ +@Slf4j +public class RocketmqSingleCollectImpl extends AbstractCollect { + + private static final int WAIT_TIMEOUT = 10; + static final int QUEUE_CAPACITY = 5000; + + private static final Set<String> SYSTEM_GROUP_SET = new HashSet<>(); + + private final ManagedExecutor executorService; + + static { + // system consumer group + SYSTEM_GROUP_SET.add(MixAll.TOOLS_CONSUMER_GROUP); + SYSTEM_GROUP_SET.add(MixAll.FILTERSRV_CONSUMER_GROUP); + SYSTEM_GROUP_SET.add(MixAll.SELF_TEST_CONSUMER_GROUP); + SYSTEM_GROUP_SET.add(MixAll.ONS_HTTP_PROXY_GROUP); + SYSTEM_GROUP_SET.add(MixAll.CID_ONSAPI_PULL_GROUP); + SYSTEM_GROUP_SET.add(MixAll.CID_ONSAPI_PERMISSION_GROUP); + SYSTEM_GROUP_SET.add(MixAll.CID_ONSAPI_OWNER_GROUP); + SYSTEM_GROUP_SET.add(MixAll.CID_SYS_RMQ_TRANS); + } + + public RocketmqSingleCollectImpl() { + this(createExecutor()); + } + + RocketmqSingleCollectImpl(ManagedExecutor executorService) { + this.executorService = executorService; + } + + private static ManagedExecutor createExecutor() { + Runtime runtime = Runtime.getRuntime(); + int corePoolSize = Math.max(8, runtime.availableProcessors()); + int maximumPoolSize = Math.max(16, runtime.availableProcessors()); + Thread.UncaughtExceptionHandler handler = (thread, throwable) -> { + log.error("RocketMQCollectGroup has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }; + return ManagedExecutors.newDiscardOldestVirtualExecutor("rocketmq-collector", "rocketmq-collector-", + corePoolSize, maximumPoolSize, QUEUE_CAPACITY, handler); + } + + public void destroy() { + this.executorService.close(); + } + + /** + * preCheck params + * @param metrics metrics config + */ + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + require(metrics != null && metrics.getRocketmq() != null, "Rocketmq collect must has rocketmq params"); + RocketmqProtocol rocketmq = metrics.getRocketmq(); + requireHasText(rocketmq.getNamesrvHost(), "Rocketmq Protocol namesrvHost is required."); + requireHasText(rocketmq.getNamesrvPort(), "Rocketmq Protocol namesrvPort is required."); + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) { + DefaultMQAdminExt mqAdminExt = null; + try { + mqAdminExt = this.createMqAdminExt(metrics); + mqAdminExt.start(); + + RocketmqCollectData rocketmqCollectData = new RocketmqCollectData(); + this.collectData(mqAdminExt, rocketmqCollectData); + + this.fillBuilder(rocketmqCollectData, builder, metrics.getAliasFields(), metrics.getRocketmq().getParseScript()); + + } catch (Exception e) { + builder.setCode(CollectRep.Code.FAIL); + String message = CommonUtil.getMessageFromThrowable(e); + builder.setMsg(message); + } finally { + if (mqAdminExt != null) { + mqAdminExt.shutdown(); + } + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_ROCKETMQ; + } + + + /** + * create the DefaultMQAdminExt + * one problem the DefaultMQAdminExt can not reuse + * @param metrics metrics + * @return DefaultMQAdminExt + */ + private DefaultMQAdminExt createMqAdminExt(Metrics metrics) { + RocketmqProtocol rocketmqProtocol = metrics.getRocketmq(); + assert rocketmqProtocol != null; + RPCHook rpcHook = null; + if (StringUtils.isNotBlank(rocketmqProtocol.getAccessKey()) && StringUtils.isNotBlank(rocketmqProtocol.getSecretKey())) { + rpcHook = new AclClientRPCHook(new SessionCredentials(rocketmqProtocol.getAccessKey(), rocketmqProtocol.getSecretKey())); + } + DefaultMQAdminExt mqAdminExt = new DefaultMQAdminExt(rpcHook, 5000L); + mqAdminExt.setNamesrvAddr(rocketmqProtocol.getNamesrvHost() + ":" + rocketmqProtocol.getNamesrvPort()); + mqAdminExt.setInstanceName("admin-" + System.currentTimeMillis()); + return mqAdminExt; + } + + /** + * collect rocketmq data + * @param mqAdminExt rocketmq rpc admin + * @param rocketmqCollectData rocketmq data + * @throws Exception when rpc error + */ + private void collectData(DefaultMQAdminExt mqAdminExt, RocketmqCollectData rocketmqCollectData) throws Exception { + this.collectClusterData(mqAdminExt, rocketmqCollectData); + this.collectConsumerData(mqAdminExt, rocketmqCollectData); + this.collectTopicData(mqAdminExt, rocketmqCollectData); + } + + /** + * collect rocketmq cluster data + * @param mqAdminExt rocketmq rpc admin + * @param rocketmqCollectData rocketmq data + * @throws Exception when rpc error + */ + private void collectClusterData(DefaultMQAdminExt mqAdminExt, RocketmqCollectData rocketmqCollectData) throws Exception { + try { + List<RocketmqCollectData.ClusterBrokerData> clusterBrokerDataList = new ArrayList<>(); + rocketmqCollectData.setClusterBrokerDataList(clusterBrokerDataList); + + ClusterInfo clusterInfo = mqAdminExt.examineBrokerClusterInfo(); + for (BrokerData brokerData : clusterInfo.getBrokerAddrTable().values()) { + + for (Map.Entry<Long, String> entry : brokerData.getBrokerAddrs().entrySet()) { + RocketmqCollectData.ClusterBrokerData clusterBrokerData = new RocketmqCollectData.ClusterBrokerData(); + clusterBrokerDataList.add(clusterBrokerData); + + clusterBrokerData.setBrokerId(entry.getKey()); + clusterBrokerData.setAddress(entry.getValue()); + + KVTable kvTable = mqAdminExt.fetchBrokerRuntimeStats(entry.getValue()); + clusterBrokerData.setVersion(kvTable.getTable().get("brokerVersionDesc")); + + String putTps = kvTable.getTable().get("putTps"); + if (StringUtils.isNotEmpty(putTps)) { + String[] putTpsArr = putTps.split(" "); + clusterBrokerData.setProducerMessageTps(Double.parseDouble(putTpsArr[0])); + } + + String getTransferredTps = kvTable.getTable().get("getTransferedTps"); + if (StringUtils.isNotEmpty(getTransferredTps)) { + String[] getTransferredTpsArr = getTransferredTps.split(" "); + clusterBrokerData.setConsumerMessageTps(Double.parseDouble(getTransferredTpsArr[0])); + } + + String msgPutTotalTodayMorning = kvTable.getTable().get("msgPutTotalTodayMorning"); + String msgPutTotalYesterdayMorning = kvTable.getTable().get("msgPutTotalYesterdayMorning"); + if (StringUtils.isNotEmpty(msgPutTotalTodayMorning) && StringUtils.isNotEmpty(msgPutTotalYesterdayMorning)) { + long yesterdayProduceCount = Long.parseLong(msgPutTotalTodayMorning) - Long.parseLong(msgPutTotalYesterdayMorning); + clusterBrokerData.setYesterdayProduceCount(yesterdayProduceCount); + } + + String msgGetTotalTodayMorning = kvTable.getTable().get("msgGetTotalTodayMorning"); + String msgGetTotalYesterdayMorning = kvTable.getTable().get("msgGetTotalYesterdayMorning"); + if (StringUtils.isNotEmpty(msgGetTotalTodayMorning) && StringUtils.isNotEmpty(msgGetTotalYesterdayMorning)) { + long yesterdayConsumerCount = Long.parseLong(msgGetTotalTodayMorning) - Long.parseLong(msgGetTotalYesterdayMorning); + clusterBrokerData.setYesterdayConsumeCount(yesterdayConsumerCount); + } + + String msgPutTotalTodayNow = kvTable.getTable().get("msgPutTotalTodayNow"); + if (StringUtils.isNotEmpty(msgPutTotalTodayNow) && StringUtils.isNotEmpty(msgPutTotalTodayMorning)) { + long todayProduceCount = Long.parseLong(msgPutTotalTodayNow) - Long.parseLong(msgPutTotalTodayMorning); + clusterBrokerData.setTodayProduceCount(todayProduceCount); + } + + String msgGetTotalTodayNow = kvTable.getTable().get("msgGetTotalTodayNow"); + if (StringUtils.isNotEmpty(msgGetTotalTodayNow) && StringUtils.isNotEmpty(msgGetTotalTodayMorning)) { + long todayConsumerCount = Long.parseLong(msgGetTotalTodayNow) - Long.parseLong(msgGetTotalTodayMorning); + clusterBrokerData.setTodayConsumeCount(todayConsumerCount); + } + } + } + } catch (Exception e) { + log.warn("collect rocketmq cluster data error", e); + throw e; + } + } + + /** + * collect rocketmq consumer data + * @param mqAdminExt rocketmq rpc admin + * @param rocketmqCollectData rocketmq data + * @throws Exception when rpc error + */ + private void collectConsumerData(DefaultMQAdminExt mqAdminExt, RocketmqCollectData rocketmqCollectData) throws Exception { + Set<String> consumerGroupSet = new HashSet<>(); + try { + // get consumerGroup + ClusterInfo clusterInfo = mqAdminExt.examineBrokerClusterInfo(); + for (BrokerData brokerData : clusterInfo.getBrokerAddrTable().values()) { + SubscriptionGroupWrapper subscriptionGroupWrapper = mqAdminExt.getAllSubscriptionGroup(brokerData.selectBrokerAddr(), 3000L); + consumerGroupSet.addAll(subscriptionGroupWrapper.getSubscriptionGroupTable().keySet()); + } + + List<RocketmqCollectData.ConsumerInfo> consumerInfoList = Collections.synchronizedList(Lists.newArrayList()); + rocketmqCollectData.setConsumerInfoList(consumerInfoList); + CountDownLatch countDownLatch = new CountDownLatch(consumerGroupSet.size()); + for (String consumerGroup : consumerGroupSet) { + if (SYSTEM_GROUP_SET.contains(consumerGroup)) { + continue; + } + executeConsumerTask(() -> { + RocketmqCollectData.ConsumerInfo consumerInfo = new RocketmqCollectData.ConsumerInfo(); + consumerInfoList.add(consumerInfo); + consumerInfo.setConsumerGroup(consumerGroup); + try { + ConsumeStats consumeStats = null; + try { + consumeStats = mqAdminExt.examineConsumeStats(consumerGroup); + } catch (Exception e) { + log.warn("examineConsumeStats exception to consumerGroup {}, response [{}]", consumerGroup, e.getMessage()); + } + if (consumeStats != null) { + consumerInfo.setConsumeTps(consumeStats.getConsumeTps()); + consumerInfo.setDiffTotal(consumeStats.computeTotalDiff()); + } + + ConsumerConnection consumerConnection = null; + try { + consumerConnection = mqAdminExt.examineConsumerConnectionInfo(consumerGroup); + } catch (Exception e) { + log.warn("examineConsumeStats exception to consumerGroup {}, response [{}]", consumerGroup, e.getMessage()); + } + if (consumerConnection != null) { + consumerInfo.setClientQuantity(consumerConnection.getConnectionSet().size()); + consumerInfo.setMessageModel(consumerConnection.getMessageModel().getModeCN()); + consumerInfo.setConsumeType(consumerConnection.getConsumeType().getTypeCN()); + } + } catch (Exception e) { + log.warn("examineConsumeStats or examineConsumerConnectionInfo error, {}", consumerGroup, e); + } finally { + countDownLatch.countDown(); + } + }); + } + + if (!countDownLatch.await(WAIT_TIMEOUT, TimeUnit.SECONDS)) { + log.warn("examineConsumeStats or examineConsumerConnectionInfo timeout"); + } + } catch (Exception e) { + log.warn("collect rocketmq consume data error", e); + throw e; + } + } + + /** + * collect topic data + * @param mqAdminExt rocketmq rpc admin + * @param rocketmqCollectData rocketmq data + * @throws Exception when rpc error + */ + private void collectTopicData(DefaultMQAdminExt mqAdminExt, RocketmqCollectData rocketmqCollectData) throws Exception { + try { + TopicList topicList = mqAdminExt.fetchAllTopicList(); + Set<String> topics = topicList.getTopicList() + .stream() + .filter(topic -> !(topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) || topic.startsWith(MixAll.DLQ_GROUP_TOPIC_PREFIX))) + .collect(Collectors.toSet()); + List<Map<String /* topic */, List<RocketmqCollectData.TopicQueueInfo>>> topicInfoList = new ArrayList<>(); + for (String topic : topics) { + Map<String, List<RocketmqCollectData.TopicQueueInfo>> topicQueueInfoTable = new HashMap<>(32); + List<RocketmqCollectData.TopicQueueInfo> topicQueueInfoList = new ArrayList<>(); + + // When querying queue information for a topic, you need to use a for-loop to call mqAdminExt.examineTopicStats(). + // If the number of topics is large, the number of calls will also be high + topicQueueInfoTable.put(topic, topicQueueInfoList); + topicInfoList.add(topicQueueInfoTable); + rocketmqCollectData.setTopicInfoList(topicInfoList); + } + } catch (Exception e) { + log.warn("collect rocketmq topic data error", e); + throw e; + } + } + + /** + * fill data to builder + * + * @param rocketmqCollectData rocketmq data + * @param builder metrics data builder + * @param aliasFields alia fields + * @param parseScript JSON base path + */ + private void fillBuilder(RocketmqCollectData rocketmqCollectData, CollectRep.MetricsData.Builder builder, List<String> aliasFields, String parseScript) { + String dataJson = JSONObject.toJSONString(rocketmqCollectData); + List<Object> results = JsonPathParser.parseContentWithJsonPath(dataJson, parseScript); + for (int i = 0; i < results.size(); i++) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String aliasField : aliasFields) { + List<Object> valueList = JsonPathParser.parseContentWithJsonPath(dataJson, parseScript + aliasField); + if (CollectionUtils.isNotEmpty(valueList) && valueList.size() > i) { + Object value = valueList.get(i); + valueRowBuilder.addColumn(value == null ? CommonConstants.NULL_VALUE : String.valueOf(value)); + } else { + valueRowBuilder.addColumn(CommonConstants.NULL_VALUE); + } + } + builder.addValueRow(valueRowBuilder.build()); + } + } + + void executeConsumerTask(Runnable runnable) { + executorService.execute(runnable); + } + + private static void require(boolean expression, String message) { + if (!expression) { + throw new IllegalArgumentException(message); + } + } + + private static void requireHasText(String value, String message) { + require(value != null && !value.trim().isEmpty(), message); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-rocketmq/src/test/java/org/apache/hertzbeat/collector/collect/rocketmq/RocketmqSingleCollectTest.java b/hertzbeat-collector/hertzbeat-collector-rocketmq/src/test/java/org/apache/hertzbeat/collector/collect/rocketmq/RocketmqSingleCollectTest.java new file mode 100644 index 0000000..b40ed25 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-rocketmq/src/test/java/org/apache/hertzbeat/collector/collect/rocketmq/RocketmqSingleCollectTest.java @@ -0,0 +1,162 @@ +/* + * 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.collector.collect.rocketmq; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.concurrent.ManagedExecutor; +import org.apache.hertzbeat.common.concurrent.ManagedExecutors; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.RocketmqProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link RocketmqSingleCollectImpl} + */ +public class RocketmqSingleCollectTest { + private RocketmqSingleCollectImpl collect; + + @BeforeEach + public void setUp() throws Exception { + collect = new RocketmqSingleCollectImpl(); + } + + @AfterEach + void tearDown() { + if (collect != null) { + collect.destroy(); + } + } + + @Test + void preCheck() { + // metrics is null + assertThrows(IllegalArgumentException.class, () -> { + collect.preCheck(null); + }); + + // rocketmq is null + assertThrows(IllegalArgumentException.class, () -> { + collect.preCheck(Metrics.builder().build()); + }); + + // rocketmq srv host is null + assertThrows(IllegalArgumentException.class, () -> { + RocketmqProtocol mq = new RocketmqProtocol(); + collect.preCheck(Metrics.builder().rocketmq(mq).build()); + }); + + // rocketmq srv port is null + assertThrows(IllegalArgumentException.class, () -> { + RocketmqProtocol mq = RocketmqProtocol.builder().namesrvHost("127.0.0.1").build(); + collect.preCheck(Metrics.builder().rocketmq(mq).build()); + }); + + // no exception throw + assertDoesNotThrow(() -> { + RocketmqProtocol mq = RocketmqProtocol.builder().namesrvHost("127.0.0.1").namesrvPort("9876").build(); + collect.preCheck(Metrics.builder().rocketmq(mq).build()); + }); + } + + @Test + void destroy() { + assertDoesNotThrow(() -> { + collect.destroy(); + }); + } + + @Test + void collect() { + // metrics is null + assertDoesNotThrow(() -> { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + collect.collect(builder, null); + }); + + assertDoesNotThrow(() -> { + CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder(); + RocketmqProtocol mq = RocketmqProtocol.builder().namesrvHost("127.0.0.1").namesrvPort("9876").build(); + Metrics metrics = Metrics.builder().rocketmq(mq).build(); + collect.collect(builder, metrics); + }); + } + + @Test + void supportProtocol() { + assertEquals(DispatchConstants.PROTOCOL_ROCKETMQ, collect.supportProtocol()); + } + + @Test + void executeConsumerTaskRunsOnVirtualThread() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + + collect.executeConsumerTask(() -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void executeConsumerTaskDiscardsOldestWhenQueueIsFull() throws InterruptedException { + ManagedExecutor executor = ManagedExecutors.newDiscardOldestVirtualExecutor("rocketmq-test", + "rocketmq-test-", 1, 1, 1, (thread, throwable) -> { + }); + RocketmqSingleCollectImpl testCollect = new RocketmqSingleCollectImpl(executor); + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch thirdStarted = new CountDownLatch(1); + AtomicBoolean secondExecuted = new AtomicBoolean(false); + try { + testCollect.executeConsumerTask(() -> { + firstStarted.countDown(); + try { + releaseFirst.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + testCollect.executeConsumerTask(() -> secondExecuted.set(true)); + testCollect.executeConsumerTask(thirdStarted::countDown); + + releaseFirst.countDown(); + assertTrue(thirdStarted.await(5, TimeUnit.SECONDS)); + assertFalse(secondExecuted.get()); + } finally { + releaseFirst.countDown(); + testCollect.destroy(); + } + } +} diff --git a/hertzbeat-collector/mvnw b/hertzbeat-collector/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/hertzbeat-collector/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash> +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/hertzbeat-collector/mvnw.cmd b/hertzbeat-collector/mvnw.cmd new file mode 100644 index 0000000..b150b91 --- /dev/null +++ b/hertzbeat-collector/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash> +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/hertzbeat-collector/pom.xml b/hertzbeat-collector/pom.xml new file mode 100644 index 0000000..f1e1454 --- /dev/null +++ b/hertzbeat-collector/pom.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>hertzbeat</artifactId> + <groupId>org.apache.hertzbeat</groupId> + <version>2.0-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>hertzbeat-collector</artifactId> + <name>${project.artifactId}</name> + <packaging>pom</packaging> + <properties> + <java.version>25</java.version> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <mysql.r2dbc.reactor.bom.version>2024.0.3</mysql.r2dbc.reactor.bom.version> + </properties> + + <modules> + <module>hertzbeat-collector-basic</module> + <module>hertzbeat-collector-common</module> + <module>hertzbeat-collector-collector</module> + <module>hertzbeat-collector-mysql-r2dbc</module> + <module>hertzbeat-collector-mongodb</module> + <module>hertzbeat-collector-nebulagraph</module> + <module>hertzbeat-collector-rocketmq</module> + <module>hertzbeat-collector-kafka</module> + </modules> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>io.projectreactor</groupId> + <artifactId>reactor-bom</artifactId> + <version>${mysql.r2dbc.reactor.bom.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-common</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-basic</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-mysql-r2dbc</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-mongodb</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-kafka</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-nebulagraph</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-rocketmq</artifactId> + <version>${hertzbeat.version}</version> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-common-core</artifactId> + </dependency> + </dependencies> + + <build> + <finalName>apache-${project.artifactId}-${hzb.version}</finalName> + </build> + +</project> diff --git a/hertzbeat-common-core/pom.xml b/hertzbeat-common-core/pom.xml new file mode 100644 index 0000000..949cf19 --- /dev/null +++ b/hertzbeat-common-core/pom.xml @@ -0,0 +1,184 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>hertzbeat</artifactId> + <groupId>org.apache.hertzbeat</groupId> + <version>2.0-SNAPSHOT</version> + </parent> + + <modelVersion>4.0.0</modelVersion> + + <artifactId>hertzbeat-common-core</artifactId> + <name>${project.artifactId}</name> + <description>Framework-agnostic shared runtime models, utilities, and protocol support.</description> + + <dependencies> + <!-- Tool dependencies --> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + </dependency> + <dependency> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + </dependency> + <dependency> + <groupId>com.google.protobuf</groupId> + <artifactId>protobuf-java-util</artifactId> + </dependency> + <!-- Use apache.http.conn.util instead of sun.net.util --> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + </dependency> + <!-- caffeine--> + <dependency> + <groupId>com.github.ben-manes.caffeine</groupId> + <artifactId>caffeine</artifactId> + </dependency> + <!-- kafka --> + <dependency> + <groupId>org.apache.kafka</groupId> + <artifactId>kafka-clients</artifactId> + <exclusions> + <exclusion> + <artifactId>snappy-java</artifactId> + <groupId>org.xerial.snappy</groupId> + </exclusion> + </exclusions> + </dependency> + <!-- redis --> + <dependency> + <groupId>io.lettuce</groupId> + <artifactId>lettuce-core</artifactId> + </dependency> + <!-- expr calculate --> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-jexl3</artifactId> + </dependency> + + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + </dependency> + + <!-- https://mvnrepository.com/artifact/com.beetstra.jutf7/jutf7 --> + <dependency> + <groupId>com.beetstra.jutf7</groupId> + <artifactId>jutf7</artifactId> + <version>${jutf7.version}</version> + </dependency> + + <dependency> + <groupId>org.apache.poi</groupId> + <artifactId>poi</artifactId> + <version>${poi.version}</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.apache.poi</groupId> + <artifactId>poi-ooxml</artifactId> + <version>${poi.version}</version> + <scope>compile</scope> + <exclusions> + <exclusion> + <artifactId>commons-compress</artifactId> + <groupId>org.apache.commons</groupId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-compress</artifactId> + <version>${commons-compress.version}</version> + </dependency> + + <dependency> + <groupId>org.apache.arrow</groupId> + <artifactId>arrow-vector</artifactId> + </dependency> + <dependency> + <groupId>org.apache.arrow</groupId> + <artifactId>arrow-memory-netty</artifactId> + </dependency> + + <dependency> + <groupId>org.xerial.snappy</groupId> + <artifactId>snappy-java</artifactId> + <version>${snappy-java.version}</version> + </dependency> + <dependency> + <groupId>com.github.javaparser</groupId> + <artifactId>javaparser-core</artifactId> + <version>${javaparser.version}</version> + <scope>test</scope> + </dependency> + <!-- Test dependencies --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.github.jsqlparser</groupId> + <artifactId>jsqlparser</artifactId> + </dependency> + <!-- jackson for optional json processing --> + <dependency> + <groupId>tools.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + </dependency> + + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-annotations</artifactId> + </dependency> + + <dependency> + <groupId>tools.jackson.dataformat</groupId> + <artifactId>jackson-dataformat-xml</artifactId> + </dependency> + + <dependency> + <groupId>tools.jackson.dataformat</groupId> + <artifactId>jackson-dataformat-yaml</artifactId> + </dependency> + <!-- swagger annotations (optional) --> + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <scope>provided</scope> + </dependency> + <!-- validation annotations (optional) --> + <dependency> + <groupId>jakarta.validation</groupId> + <artifactId>jakarta.validation-api</artifactId> + <scope>provided</scope> + </dependency> + <!-- jakarta annotations --> + <dependency> + <groupId>jakarta.annotation</groupId> + <artifactId>jakarta.annotation-api</artifactId> + </dependency> + </dependencies> + +</project> diff --git a/hertzbeat-common-core/src/main/java/org/apache/arrow/vector/table/ArrowTable.java b/hertzbeat-common-core/src/main/java/org/apache/arrow/vector/table/ArrowTable.java new file mode 100644 index 0000000..ee83572 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/arrow/vector/table/ArrowTable.java @@ -0,0 +1,107 @@ +/* + * 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.arrow.vector.table; + +import java.util.Iterator; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.arrow.vector.FieldVector; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.dictionary.DictionaryProvider; +import org.apache.arrow.vector.types.pojo.Schema; +import org.apache.arrow.vector.util.TransferPair; + +/** + * <p>A temporary Arrow Table implementation that uses Arrow's FieldVectors to store data.</p> + * <p>Instead of Table, due the table can not store the schema metadata, so we need to use ArrowTable to store the schema metadata.</p> + * <p>todo Improve the upstream branch `table` to support schema metadata.</p> + */ +@Slf4j +public class ArrowTable extends BaseTable { + + /** + * Constructs new instance with the given rowCount, and containing the schema and each of the + * given vectors. + * + * @param fieldVectors the FieldVectors containing the table's data + * @param rowCount the number of rows in the table + * @param provider a dictionary provider, may be null if none of the vectors in the table are + * encoded + */ + public ArrowTable(List<FieldVector> fieldVectors, int rowCount, DictionaryProvider provider) { + super(fieldVectors, rowCount, provider); + } + + /** + * Constructs a new instance containing the data from the argument. Vectors are shared between the + * Table and VectorSchemaRoot. Direct modification of those vectors is unsafe and should be + * avoided. + * + * @param vsr The VectorSchemaRoot providing data for this Table + */ + public ArrowTable(VectorSchemaRoot vsr) { + this(vsr.getFieldVectors(), vsr.getRowCount(), null); + try { + this.schema = Schema.fromJSON(vsr.getSchema().toJson()); + } catch (Exception e) { + log.error("Failed to parse arrow table schema from VectorSchemaRoot", e); + } + vsr.clear(); + } + + @Override + public VectorSchemaRoot toVectorSchemaRoot() { + List<FieldVector> fieldVectorList = fieldVectors.stream() + .map( + v -> { + TransferPair transferPair = v.getTransferPair(v.getAllocator()); + transferPair.transfer(); + return (FieldVector) transferPair.getTo(); + }) + .toList(); + VectorSchemaRoot vsr = + new VectorSchemaRoot(schema, fieldVectorList, rowCount); + clear(); + return vsr; + } + + public FieldVector getVector(String columnName) { + return super.getVector(columnName); + } + + /** Returns a Row iterator for this Table. */ + public Iterator<Row> iterator() { + + return new Iterator<Row>() { + + private final Row row = new Row(ArrowTable.this); + + @Override + public Row next() { + row.next(); + return row; + } + + @Override + public boolean hasNext() { + return row.hasNext(); + } + }; + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/cache/CommonCacheService.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/cache/CommonCacheService.java new file mode 100644 index 0000000..0cf0d40 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/cache/CommonCacheService.java @@ -0,0 +1,66 @@ +/* + * 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.cache; + +/** + * common cache service + */ +public interface CommonCacheService<K, V> { + + /** + * get cache by key use clazz + * @param key key + * @return object + */ + V get(K key); + + /** + * set cache + * @param key key + * @param value value + */ + void put(K key, V value); + + /** + * set cache and return oldValue if present + * @param key key + * @param value value + * @return old value + */ + V putAndGetOld(K key, V value); + + /** + * if contain cache by key + * @param key key + * @return true is contain + */ + boolean containsKey(K key); + + /** + * remove cache + * @param key key + * @return old value + */ + V remove(K key); + + /** + * clear cache + * @return is clear success + */ + boolean clear(); +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/AdmissionMode.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/AdmissionMode.java new file mode 100644 index 0000000..5f1a0e4 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/AdmissionMode.java @@ -0,0 +1,39 @@ +/* + * 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.concurrent; + +/** + * Task admission mode for managed executors. + */ +public enum AdmissionMode { + + /** + * Start each task on its own virtual thread without an executor-level concurrency cap. + */ + UNBOUNDED_VT, + + /** + * Reject immediately when the configured concurrency cap has been reached. + */ + LIMIT_AND_REJECT, + + /** + * Block the submitter until a permit is available. + */ + LIMIT_AND_BLOCK +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/BackgroundTaskExecutor.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/BackgroundTaskExecutor.java new file mode 100644 index 0000000..030800f --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/BackgroundTaskExecutor.java @@ -0,0 +1,48 @@ +/* + * 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.concurrent; + +import java.util.concurrent.RejectedExecutionException; + +/** + * Generic background task executor abstraction for runtime components. + */ +public interface BackgroundTaskExecutor { + + /** + * Execute a short-lived task. + * + * @param runnable task + * @throws RejectedExecutionException when execution is rejected + */ + void execute(Runnable runnable) throws RejectedExecutionException; + + /** + * Execute a long-lived background task. + * + * @param runnable task + */ + void executeLongRunning(Runnable runnable); + + /** + * Release executor resources. + * + * @throws Exception close exception + */ + void destroy() throws Exception; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/ManagedExecutor.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/ManagedExecutor.java new file mode 100644 index 0000000..c86d7a9 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/ManagedExecutor.java @@ -0,0 +1,36 @@ +/* + * 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.concurrent; + +import java.util.concurrent.Executor; + +/** + * Executor with a stable logical name and close semantics. + */ +public interface ManagedExecutor extends Executor, AutoCloseable { + + /** + * Logical executor name for logging and metrics tags. + * + * @return executor name + */ + String name(); + + @Override + void close(); +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/ManagedExecutors.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/ManagedExecutors.java new file mode 100644 index 0000000..24226de --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/concurrent/ManagedExecutors.java @@ -0,0 +1,428 @@ +/* + * 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.concurrent; + +import java.util.ArrayDeque; +import java.util.Objects; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Factory methods for managed executors. + */ +public final class ManagedExecutors { + + private ManagedExecutors() { + } + + /** + * Wrap an existing executor service. + * + * @param name executor name + * @param executorService delegate executor service + * @return managed executor wrapper + */ + public static ManagedExecutor wrap(String name, ExecutorService executorService) { + return new DefaultManagedExecutor(name, executorService, null, AdmissionMode.UNBOUNDED_VT); + } + + /** + * Create a per-task virtual-thread executor with optional admission control. + * + * @param name executor name + * @param threadNamePrefix thread name prefix + * @param mode admission mode + * @param maxConcurrentTasks max concurrent tasks for limited modes + * @param handler uncaught exception handler + * @return managed executor + */ + public static ManagedExecutor newVirtualExecutor(String name, String threadNamePrefix, AdmissionMode mode, + int maxConcurrentTasks, Thread.UncaughtExceptionHandler handler) { + ThreadFactory threadFactory = Thread.ofVirtual() + .name(threadNamePrefix, 0) + .uncaughtExceptionHandler(handler) + .factory(); + ExecutorService executorService = Executors.newThreadPerTaskExecutor(threadFactory); + return new DefaultManagedExecutor(name, executorService, semaphore(mode, maxConcurrentTasks), mode); + } + + /** + * Create a platform-thread-per-task executor for a small number of long-running tasks. + * + * @param name executor name + * @param threadNamePrefix thread name prefix + * @param handler uncaught exception handler + * @return managed executor + */ + public static ManagedExecutor newPlatformExecutor(String name, String threadNamePrefix, + Thread.UncaughtExceptionHandler handler) { + ThreadFactory threadFactory = Thread.ofPlatform() + .daemon(true) + .name(threadNamePrefix, 0) + .uncaughtExceptionHandler(handler) + .factory(); + ExecutorService executorService = Executors.newThreadPerTaskExecutor(threadFactory); + return new DefaultManagedExecutor(name, executorService, null, AdmissionMode.UNBOUNDED_VT); + } + + /** + * Create a queued executor that preserves queue semantics while executing tasks on virtual threads. + * + * @param name executor name + * @param threadNamePrefix virtual-thread name prefix + * @param maxConcurrentTasks max concurrent tasks + * @param queueCapacity queue capacity, {@code <= 0} means unbounded + * @param handler uncaught exception handler + * @return managed executor + */ + public static ManagedExecutor newQueuedVirtualExecutor(String name, String threadNamePrefix, int maxConcurrentTasks, + int queueCapacity, Thread.UncaughtExceptionHandler handler) { + if (maxConcurrentTasks <= 0) { + throw new IllegalArgumentException("maxConcurrentTasks must be greater than zero for queued executors"); + } + return new QueuedVirtualManagedExecutor(name, threadNamePrefix, maxConcurrentTasks, queueCapacity, handler); + } + + /** + * Create a virtual-thread executor that preserves {@link java.util.concurrent.ThreadPoolExecutor} + * core/max/queue semantics with discard-oldest overflow handling. + * + * @param name executor name + * @param threadNamePrefix virtual-thread name prefix + * @param coreConcurrentTasks core concurrent tasks + * @param maxConcurrentTasks max concurrent tasks + * @param queueCapacity queue capacity + * @param handler uncaught exception handler + * @return managed executor + */ + public static ManagedExecutor newDiscardOldestVirtualExecutor(String name, String threadNamePrefix, + int coreConcurrentTasks, int maxConcurrentTasks, + int queueCapacity, + Thread.UncaughtExceptionHandler handler) { + if (coreConcurrentTasks <= 0) { + throw new IllegalArgumentException("coreConcurrentTasks must be greater than zero"); + } + if (maxConcurrentTasks < coreConcurrentTasks) { + throw new IllegalArgumentException("maxConcurrentTasks must be greater than or equal to coreConcurrentTasks"); + } + if (queueCapacity <= 0) { + throw new IllegalArgumentException("queueCapacity must be greater than zero"); + } + return new DiscardOldestVirtualManagedExecutor(name, threadNamePrefix, coreConcurrentTasks, + maxConcurrentTasks, queueCapacity, handler); + } + + private static Semaphore semaphore(AdmissionMode mode, int maxConcurrentTasks) { + if (mode == AdmissionMode.UNBOUNDED_VT) { + return null; + } + if (maxConcurrentTasks <= 0) { + throw new IllegalArgumentException("maxConcurrentTasks must be greater than zero for limited executors"); + } + return new Semaphore(maxConcurrentTasks); + } + + private static final class DefaultManagedExecutor implements ManagedExecutor { + + private final String name; + private final ExecutorService delegate; + private final Semaphore permits; + private final AdmissionMode admissionMode; + + private DefaultManagedExecutor(String name, ExecutorService delegate, Semaphore permits, + AdmissionMode admissionMode) { + this.name = Objects.requireNonNull(name, "name"); + this.delegate = Objects.requireNonNull(delegate, "delegate"); + this.permits = permits; + this.admissionMode = Objects.requireNonNull(admissionMode, "admissionMode"); + } + + @Override + public String name() { + return name; + } + + @Override + public void execute(Runnable command) { + Objects.requireNonNull(command, "command"); + acquirePermit(); + boolean submitted = false; + try { + delegate.execute(() -> { + try { + command.run(); + } finally { + releasePermit(); + } + }); + submitted = true; + } finally { + if (!submitted) { + releasePermit(); + } + } + } + + @Override + public void close() { + delegate.shutdownNow(); + } + + private void acquirePermit() { + if (permits == null) { + return; + } + switch (admissionMode) { + case LIMIT_AND_REJECT: + if (!permits.tryAcquire()) { + throw new RejectedExecutionException(name + " rejected task because concurrency limit was reached"); + } + break; + case LIMIT_AND_BLOCK: + try { + permits.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RejectedExecutionException(name + " interrupted while waiting for an execution permit", e); + } + break; + case UNBOUNDED_VT: + break; + default: + throw new IllegalStateException("Unsupported admission mode: " + admissionMode); + } + } + + private void releasePermit() { + if (permits != null) { + permits.release(); + } + } + } + + private static final class QueuedVirtualManagedExecutor implements ManagedExecutor { + + private final String name; + private final ExecutorService delegate; + private final ExecutorService dispatcher; + private final BlockingDeque<Runnable> queue; + private final Semaphore permits; + private final Semaphore permitSignals; + private final AtomicBoolean closed; + + private QueuedVirtualManagedExecutor(String name, String threadNamePrefix, int maxConcurrentTasks, + int queueCapacity, Thread.UncaughtExceptionHandler handler) { + this.name = Objects.requireNonNull(name, "name"); + ThreadFactory virtualFactory = Thread.ofVirtual() + .name(threadNamePrefix, 0) + .uncaughtExceptionHandler(handler) + .factory(); + this.delegate = Executors.newThreadPerTaskExecutor(virtualFactory); + this.queue = queueCapacity > 0 ? new LinkedBlockingDeque<>(queueCapacity) : new LinkedBlockingDeque<>(); + this.permits = new Semaphore(maxConcurrentTasks); + this.permitSignals = new Semaphore(0); + this.closed = new AtomicBoolean(false); + ThreadFactory dispatcherFactory = Thread.ofPlatform() + .daemon(true) + .name(threadNamePrefix + "dispatcher-", 0) + .uncaughtExceptionHandler(handler) + .factory(); + this.dispatcher = Executors.newSingleThreadExecutor(dispatcherFactory); + this.dispatcher.execute(this::dispatchLoop); + } + + @Override + public String name() { + return name; + } + + @Override + public void execute(Runnable command) { + Objects.requireNonNull(command, "command"); + if (closed.get()) { + throw new RejectedExecutionException(name + " rejected task because executor is closed"); + } + if (!queue.offerLast(command)) { + throw new RejectedExecutionException(name + " rejected task because queue capacity was reached"); + } + } + + @Override + public void close() { + if (!closed.compareAndSet(false, true)) { + return; + } + dispatcher.shutdownNow(); + delegate.shutdownNow(); + queue.clear(); + } + + private void dispatchLoop() { + try { + while (!Thread.currentThread().isInterrupted()) { + Runnable command = queue.takeFirst(); + if (!permits.tryAcquire()) { + queue.putFirst(command); + permitSignals.acquire(); + continue; + } + submit(command); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void submit(Runnable command) { + boolean submitted = false; + try { + delegate.execute(() -> { + try { + command.run(); + } finally { + permits.release(); + permitSignals.release(); + } + }); + submitted = true; + } finally { + if (!submitted) { + permits.release(); + permitSignals.release(); + } + } + } + } + + private static final class DiscardOldestVirtualManagedExecutor implements ManagedExecutor { + + private final String name; + private final ExecutorService delegate; + private final int coreConcurrentTasks; + private final int maxConcurrentTasks; + private final ArrayDeque<Runnable> queue; + private final int queueCapacity; + private final Object lock; + private boolean closed; + private int runningTasks; + + private DiscardOldestVirtualManagedExecutor(String name, String threadNamePrefix, int coreConcurrentTasks, + int maxConcurrentTasks, int queueCapacity, + Thread.UncaughtExceptionHandler handler) { + this.name = Objects.requireNonNull(name, "name"); + ThreadFactory virtualFactory = Thread.ofVirtual() + .name(threadNamePrefix, 0) + .uncaughtExceptionHandler(handler) + .factory(); + this.delegate = Executors.newThreadPerTaskExecutor(virtualFactory); + this.coreConcurrentTasks = coreConcurrentTasks; + this.maxConcurrentTasks = maxConcurrentTasks; + this.queueCapacity = queueCapacity; + this.queue = new ArrayDeque<>(queueCapacity); + this.lock = new Object(); + this.closed = false; + this.runningTasks = 0; + } + + @Override + public String name() { + return name; + } + + @Override + public void execute(Runnable command) { + Objects.requireNonNull(command, "command"); + Runnable taskToStart = null; + synchronized (lock) { + if (closed) { + throw new RejectedExecutionException(name + " rejected task because executor is closed"); + } + if (runningTasks < coreConcurrentTasks) { + runningTasks++; + taskToStart = command; + } else if (queue.size() < queueCapacity) { + queue.offerLast(command); + return; + } else if (runningTasks < maxConcurrentTasks) { + runningTasks++; + taskToStart = command; + } else { + queue.pollFirst(); + queue.offerLast(command); + return; + } + } + submit(taskToStart); + } + + @Override + public void close() { + synchronized (lock) { + if (closed) { + return; + } + closed = true; + queue.clear(); + } + delegate.shutdownNow(); + } + + private void submit(Runnable command) { + boolean submitted = false; + try { + delegate.execute(() -> { + try { + command.run(); + } finally { + onTaskComplete(); + } + }); + submitted = true; + } finally { + if (!submitted) { + synchronized (lock) { + runningTasks--; + } + throw new RejectedExecutionException(name + " rejected task because delegate submission failed"); + } + } + } + + private void onTaskComplete() { + Runnable nextTask = null; + synchronized (lock) { + if (closed) { + runningTasks--; + return; + } + nextTask = queue.pollFirst(); + if (nextTask == null) { + runningTasks--; + return; + } + } + submit(nextTask); + } + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/config/BaseKafkaProperties.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/config/BaseKafkaProperties.java new file mode 100644 index 0000000..24d411c --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/config/BaseKafkaProperties.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.config; + +import lombok.Getter; +import lombok.Setter; + +/** + * Kafka properties + */ + +@Getter +@Setter +public class BaseKafkaProperties { + + /** + * kafka's connection server url + */ + private String servers = "127.0.0.1:9092"; + + /** + * The name of the topic that receives the data + */ + private String topic; + + /** + * Consumer Group ID + */ + private String groupId; + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/config/VirtualThreadProperties.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/config/VirtualThreadProperties.java new file mode 100644 index 0000000..956b8a5 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/config/VirtualThreadProperties.java @@ -0,0 +1,201 @@ +/* + * 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.config; + +import org.apache.hertzbeat.common.concurrent.AdmissionMode; + +/** + * Framework-agnostic virtual-thread runtime configuration. + */ +public record VirtualThreadProperties( + boolean enabled, + PoolProperties collector, + PoolProperties common, + PoolProperties manager, + AlerterProperties alerter, + PoolProperties warehouse, + AsyncProperties async) { + + private static final int DEFAULT_COLLECTOR_MAX_CONCURRENT_JOBS = 512; + private static final int DEFAULT_MANAGER_MAX_CONCURRENT_JOBS = 10; + private static final int DEFAULT_NOTIFY_MAX_CONCURRENT_JOBS = 64; + private static final int DEFAULT_PERIODIC_MAX_CONCURRENT_JOBS = 10; + private static final int DEFAULT_NOTIFY_MAX_CONCURRENT_PER_CHANNEL = 4; + + public VirtualThreadProperties { + collector = normalizePool(collector, PoolProperties.collectorDefaults()); + common = common == null ? PoolProperties.commonDefaults() : common; + manager = normalizePool(manager, PoolProperties.managerDefaults()); + alerter = alerter == null ? AlerterProperties.defaults() : alerter; + warehouse = warehouse == null ? PoolProperties.warehouseDefaults() : warehouse; + async = async == null ? AsyncProperties.defaults() : async; + } + + public VirtualThreadProperties() { + this(true, PoolProperties.collectorDefaults(), PoolProperties.commonDefaults(), + PoolProperties.managerDefaults(), AlerterProperties.defaults(), + PoolProperties.warehouseDefaults(), AsyncProperties.defaults()); + } + + /** + * Create a detached properties instance with runtime defaults. + * + * @return defaults instance + */ + public static VirtualThreadProperties defaults() { + return new VirtualThreadProperties(); + } + + /** + * Pool-level configuration. + */ + public record PoolProperties( + AdmissionMode mode, + int maxConcurrentJobs) { + + public PoolProperties { + mode = mode == null ? AdmissionMode.UNBOUNDED_VT : mode; + } + + public PoolProperties() { + this(AdmissionMode.UNBOUNDED_VT, 0); + } + + public static PoolProperties collectorDefaults() { + return new PoolProperties(AdmissionMode.LIMIT_AND_REJECT, defaultCollectorConcurrency()); + } + + public static PoolProperties warehouseDefaults() { + return new PoolProperties(); + } + + public static PoolProperties commonDefaults() { + return new PoolProperties(); + } + + public static PoolProperties managerDefaults() { + return new PoolProperties(AdmissionMode.LIMIT_AND_REJECT, DEFAULT_MANAGER_MAX_CONCURRENT_JOBS); + } + + public static PoolProperties alerterNotifyDefaults() { + return new PoolProperties(AdmissionMode.LIMIT_AND_REJECT, DEFAULT_NOTIFY_MAX_CONCURRENT_JOBS); + } + + private static int defaultCollectorConcurrency() { + return DEFAULT_COLLECTOR_MAX_CONCURRENT_JOBS; + } + } + + /** + * Alerter-specific executor configuration. + */ + public record AlerterProperties( + PoolProperties notifyPool, + int periodicMaxConcurrentJobs, + QueueProperties logWorker, + QueueProperties reduce, + QueueProperties windowEvaluator, + int notifyMaxConcurrentPerChannel) { + + public AlerterProperties { + notifyPool = normalizePool(notifyPool, PoolProperties.alerterNotifyDefaults()); + periodicMaxConcurrentJobs = periodicMaxConcurrentJobs <= 0 + ? DEFAULT_PERIODIC_MAX_CONCURRENT_JOBS : periodicMaxConcurrentJobs; + logWorker = normalizeQueue(logWorker, QueueProperties.logWorkerDefaults()); + reduce = normalizeQueue(reduce, QueueProperties.reduceDefaults()); + windowEvaluator = normalizeQueue(windowEvaluator, QueueProperties.windowEvaluatorDefaults()); + notifyMaxConcurrentPerChannel = notifyMaxConcurrentPerChannel <= 0 + ? DEFAULT_NOTIFY_MAX_CONCURRENT_PER_CHANNEL : notifyMaxConcurrentPerChannel; + } + + public AlerterProperties() { + this(PoolProperties.alerterNotifyDefaults(), DEFAULT_PERIODIC_MAX_CONCURRENT_JOBS, + QueueProperties.logWorkerDefaults(), QueueProperties.reduceDefaults(), + QueueProperties.windowEvaluatorDefaults(), DEFAULT_NOTIFY_MAX_CONCURRENT_PER_CHANNEL); + } + + public static AlerterProperties defaults() { + return new AlerterProperties(); + } + } + + /** + * Queue-preserving executor configuration. + */ + public record QueueProperties( + int maxConcurrentJobs, + int queueCapacity) { + + public QueueProperties() { + this(0, 0); + } + + public static QueueProperties reduceDefaults() { + return new QueueProperties(2, 0); + } + + public static QueueProperties logWorkerDefaults() { + return new QueueProperties(10, 1000); + } + + public static QueueProperties windowEvaluatorDefaults() { + return new QueueProperties(2, 0); + } + } + + /** + * Async executor configuration. + */ + public record AsyncProperties( + boolean enabled, + int concurrencyLimit, + boolean rejectWhenLimitReached, + long taskTerminationTimeout) { + + public AsyncProperties() { + this(true, 256, true, 5000L); + } + + public static AsyncProperties defaults() { + return new AsyncProperties(); + } + } + + private static PoolProperties normalizePool(PoolProperties configured, PoolProperties defaults) { + if (configured == null) { + return defaults; + } + if (configured.mode() != AdmissionMode.UNBOUNDED_VT && configured.maxConcurrentJobs() <= 0) { + return new PoolProperties(configured.mode(), defaults.maxConcurrentJobs()); + } + return configured; + } + + private static QueueProperties normalizeQueue(QueueProperties configured, QueueProperties defaults) { + if (configured == null) { + return defaults; + } + int maxConcurrentJobs = configured.maxConcurrentJobs() <= 0 + ? defaults.maxConcurrentJobs() : configured.maxConcurrentJobs(); + int queueCapacity = configured.queueCapacity(); + if (queueCapacity <= 0 && defaults.queueCapacity() > 0) { + queueCapacity = defaults.queueCapacity(); + } + return new QueueProperties(maxConcurrentJobs, queueCapacity); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/AiConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/AiConstants.java new file mode 100644 index 0000000..f5ebf2d --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/AiConstants.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.constants; + +/** + * AI constants + */ +public interface AiConstants { + + /** + * zhiPu constants + */ + interface ZhiPuConstants { + + /** + * zhiPu request url + */ + String URL = "https://open.bigmodel.cn/api/paas/v4/chat/completions"; + + /** + * request role param + */ + String REQUEST_ROLE = "user"; + + /** + * The model outputs the maximum tokens, with a maximum output of 8192 and a default value of 3072 + */ + Integer MAX_TOKENS = 3072; + + /** + * The sampling temperature, which controls the randomness of the output, must be positive + * The value ranges from 0.0 to 1.0, and cannot be equal to 0. The default value is 0.95. The larger the value, + * the more random and creative the output will be. The smaller the value, the more stable or certain the output will be + * You are advised to adjust top_p or temperature parameters based on application scenarios, but do not adjust the two parameters at the same time + */ + float TEMPERATURE = 0.7f; + + } + + /** + * alibaba Ai constants + */ + interface AliAiConstants { + + /** + * alibabaAi request url + */ + String URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"; + + /** + * request role param + */ + String REQUEST_ROLE = "user"; + + /** + * The model outputs the maximum tokens, with a maximum output of 8192 and a default value of 3072 + */ + Integer MAX_TOKENS = 3072; + + /** + * The sampling temperature, which controls the randomness of the output, must be positive + * The value ranges from 0.0 to 1.0, and cannot be equal to 0. The default value is 0.95. The larger the value, + * the more random and creative the output will be. The smaller the value, the more stable or certain the output will be + * You are advised to adjust top_p or temperature parameters based on application scenarios, but do not adjust the two parameters at the same time + */ + float TEMPERATURE = 0.7f; + + + } + + /** + * Kimi Ai constants + */ + interface KimiAiConstants { + + /** + * Kimi Ai URL + */ + String URL = "https://api.moonshot.cn/v1/chat/completions"; + + /** + * request role param + */ + String REQUEST_ROLE = "user"; + + /** + * The model outputs the maximum tokens, with a maximum output of 8192 and a default value of 3072 + */ + Integer MAX_TOKENS = 3072; + + /** + * The sampling temperature, which controls the randomness of the output, must be positive + * The value ranges from 0.0 to 1.0, and cannot be equal to 0. The default value is 0.95. The larger the value, + * the more random and creative the output will be. The smaller the value, the more stable or certain the output will be + * You are advised to adjust top_p or temperature parameters based on application scenarios, but do not adjust the two parameters at the same time + */ + float TEMPERATURE = 0.7f; + + + } + + /** + * sparkDesk constants + */ + interface SparkDeskConstants { + /** + * SparkDesk Ai URL + */ + String SPARK_ULTRA_URL = "https://spark-api-open.xf-yun.com/v1/chat/completions"; + + /** + * request role param + */ + String REQUEST_ROLE = "user"; + + /** + * The model outputs the maximum tokens, with a maximum output of 8192 and a default value of 3072 + */ + Integer MAX_TOKENS = 3072; + + /** + * The sampling temperature, which controls the randomness of the output, must be positive + * The value ranges from 0.0 to 1.0, and cannot be equal to 0. The default value is 0.95. The larger the value, + * the more random and creative the output will be. The smaller the value, the more stable or certain the output will be + * You are advised to adjust top_p or temperature parameters based on application scenarios, but do not adjust the two parameters at the same time + */ + float TEMPERATURE = 0.7f; + + } + + /** + * Ollama constants + */ + interface OllamaConstants { + /** + * request role param + */ + String REQUEST_ROLE = "user"; + + /** + * The model outputs the maximum tokens, with a maximum output of 8192 and a default value of 3072 + */ + Integer MAX_TOKENS = 3072; + + /** + * The sampling temperature, which controls the randomness of the output, must be positive + * The value ranges from 0.0 to 1.0, and cannot be equal to 0. The default value is 0.95. The larger the value, + * the more random and creative the output will be. The smaller the value, the more stable or certain the output will be + * You are advised to adjust top_p or temperature parameters based on application scenarios, but do not adjust the two parameters at the same time + */ + float TEMPERATURE = 0.7f; + + } + + /** + * OpenRouter constants + */ + interface OpenRouterConstants { + /** + * OpenRouter Ai URL + */ + String URL = "https://openrouter.ai/api/v1/chat/completions"; + + /** + * request role param + */ + String REQUEST_ROLE = "user"; + + /** + * The model outputs the maximum tokens, with a maximum output of 8192 and a default value of 3072 + */ + Integer MAX_TOKENS = 3072; + + /** + * The sampling temperature, which controls the randomness of the output, must be positive + * The value ranges from 0.0 to 1.0, and cannot be equal to 0. The default value is 0.95. The larger the value, + * the more random and creative the output will be. The smaller the value, the more stable or certain the output will be + * You are advised to adjust top_p or temperature parameters based on application scenarios, but do not adjust the two parameters at the same time + */ + float TEMPERATURE = 0.7f; + + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/AiTypeEnum.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/AiTypeEnum.java new file mode 100644 index 0000000..037b7be --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/AiTypeEnum.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.constants; + +import java.util.Arrays; + +/** + * Ai type Enum + */ +public enum AiTypeEnum { + + /** + * ZhiPu + */ + zhiPu, + + /** + * sparkDesk + */ + sparkDesk, + + /** + * alibabaAi + */ + alibabaAi, + + /** + * Moonshot AI + */ + kimiAi, + + /** + * Ollama AI + */ + ollama, + + /** + * OpenRouter + */ + openRouter; + + /** + * get type + */ + public static AiTypeEnum getTypeByName(String type) { + return Arrays.stream(values()) + .filter(ai -> ai.name().equals(type)) + .findFirst() + .orElse(null); + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/CollectCodeConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/CollectCodeConstants.java new file mode 100644 index 0000000..75b4605 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/CollectCodeConstants.java @@ -0,0 +1,36 @@ +/* + * 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.constants; + +/** + * collect code + */ +public interface CollectCodeConstants { + /** collect success **/ + String SUCCESS = "0"; + /** collector not available **/ + String UN_AVAILABLE = "1"; + /** peer network un reachable(icmp) **/ + String UN_REACHABLE = "2"; + /** peer network server un connectable(tcp,udp...) **/ + String UN_CONNECTABLE = "3"; + /** collect metrics data failed(http,ssh,snmp...) **/ + String FAILED = "4"; + /** collect metrics data timeout **/ + String TIMEOUT = "5"; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/CommonConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/CommonConstants.java new file mode 100644 index 0000000..c03d872 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/CommonConstants.java @@ -0,0 +1,410 @@ +/* + * 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.constants; + +/** + * Public Common Constant + */ +public interface CommonConstants { + + /** + * Response status code: generic success + */ + byte SUCCESS_CODE = 0x00; + + /** + * Response status code: generic failure + */ + byte FAIL_CODE = 0x0F; + + /** + * Response status code: Parameter verification failed + */ + byte PARAM_INVALID_CODE = 0x01; + + /** + * Response Status Code: Probe Failed + */ + byte DETECT_FAILED_CODE = 0x02; + + /** + * Response status code: monitoring does not exist + */ + byte MONITOR_NOT_EXIST_CODE = 0x03; + + /** + * Response Status Code: Monitor Service Conflict + */ + byte MONITOR_CONFLICT_CODE = 0x04; + + /** + * Response status code: Incorrect login account password + */ + byte LOGIN_FAILED_CODE = 0x05; + + /** + * Monitoring status 0: Paused, 1: Up, 2: Down + */ + byte MONITOR_PAUSED_CODE = 0x00; + + /** + * Monitoring status 0: Paused, 1: Up, 2: Down + */ + byte MONITOR_UP_CODE = 0x01; + + /** + * Monitoring status 0: Paused, 1: Up, 2: Down + */ + byte MONITOR_DOWN_CODE = 0x02; + + /** + * scrape type static + */ + String SCRAPE_STATIC = "static"; + + /** + * scrape type + */ + String SCRAPE_HTTP_SD = "http_sd"; + + /** + * label key: instance + */ + String LABEL_INSTANCE = "instance"; + + /** + * label key: defineid + */ + String LABEL_DEFINE_ID = "defineid"; + + /** + * label key: alert name + */ + String LABEL_ALERT_NAME = "alertname"; + + /** + * label key: instance name + */ + String LABEL_INSTANCE_NAME = "instancename"; + + /** + * Alarm severity label key + */ + String LABEL_ALERT_SEVERITY = "severity"; + + /** + * Alarm severity fatal level + */ + String LABEL_ALERT_FATAL = "fatal"; + + /** + * alarm severity emergency level + */ + String ALERT_SEVERITY_EMERGENCY = "emergency"; + + /** + * alarm severity critical level + */ + String ALERT_SEVERITY_CRITICAL = "critical"; + + /** + * alarm severity warning level + */ + String ALERT_SEVERITY_WARNING = "warning"; + + /** + * alarm severity info level + */ + String ALERT_SEVERITY_INFO = "info"; + + /** + * Alarm status: firing + */ + String ALERT_STATUS_FIRING = "firing"; + + /** + * Alarm status: resolved + */ + String ALERT_STATUS_RESOLVED = "resolved"; + + /** + * Alarm status: pending + */ + String ALERT_STATUS_PENDING = "pending"; + + /** + * metric alert threshold type: realtime + */ + String METRIC_ALERT_THRESHOLD_TYPE_REALTIME = "realtime_metric"; + + /** + * metric alert threshold type: periodic + */ + String METRIC_ALERT_THRESHOLD_TYPE_PERIODIC = "periodic_metric"; + + /** + * log alert threshold type: realtime + */ + String LOG_ALERT_THRESHOLD_TYPE_REALTIME = "realtime_log"; + + /** + * log alert threshold type: periodic + */ + String LOG_ALERT_THRESHOLD_TYPE_PERIODIC = "periodic_log"; + + /** + * Alert mode label key + */ + String ALERT_MODE_LABEL = "alert_mode"; + + /** + * Alert mode: group + */ + String ALERT_MODE_GROUP = "group"; + + /** + * Alert mode: individual + */ + String ALERT_MODE_INDIVIDUAL = "individual"; + + /** + * Field parameter type: number + */ + byte TYPE_NUMBER = 0; + + /** + * Field parameter type: String + */ + byte TYPE_STRING = 1; + + /** + * Field parameter type: encrypted string + */ + byte TYPE_SECRET = 2; + + /** + * Field parameter type: time + */ + byte TYPE_TIME = 3; + + /** + * Collection metric value: null placeholder for empty value + */ + String NULL_VALUE = " "; + + /** + * + */ + String PROM_TIME = "timestamp"; + + /** + * + */ + String PROM_VALUE = "value"; + + /** + * prometheus metric value alias + */ + String PROM_METRIC_VALUE = "metric_value"; + + /** + * Parameter Type Number + */ + byte PARAM_TYPE_NUMBER = 0; + + /** + * Parameter Type String + */ + byte PARAM_TYPE_STRING = 1; + + /** + * Parameter Type Password + */ + byte PARAM_TYPE_PASSWORD = 2; + + /** + * Parameter Type Map values + */ + byte PARAM_TYPE_MAP = 3; + + /** + * Parameter Type arrays values + */ + byte PARAM_TYPE_ARRAY = 4; + + /** + * cache key notice_rule + */ + String CACHE_NOTICE_RULE = "notice_rule"; + + /** + * cache key alert silence + */ + String CACHE_ALERT_SILENCE = "alert_silence"; + + /** + * cache key metric alert define + */ + String METRIC_CACHE_ALERT_DEFINE = "metric_alert_define"; + + /** + * cache key log alert define + */ + String LOG_CACHE_ALERT_DEFINE = "log_alert_define"; + + /** + * cache key alert converge + */ + String CACHE_ALERT_GROUP_CONVERGE = "alert_group_converge"; + + /** + * cache key alert inhibit + */ + String CACHE_ALERT_INHIBIT = "alert_inhibit"; + + /** + * collector status online 0 + */ + byte COLLECTOR_STATUS_ONLINE = 0; + + /** + * collector status offline 1 + */ + byte COLLECTOR_STATUS_OFFLINE = 1; + + /** + * default main collector name + */ + String MAIN_COLLECTOR_NODE = "main-default-collector"; + + /** + * locale spilt + */ + String LOCALE_SEPARATOR = "_"; + + /** + * ignore label + * Handle situations where recovery alarms are not configured, but need to be used to change task state + */ + String IGNORE = "ignore"; + + /** + * collector mode public + */ + String MODE_PUBLIC = "public"; + + /** + * collector mode private + */ + String MODE_PRIVATE = "private"; + + /** + * collector auth failed message + */ + String COLLECTOR_AUTH_FAILED = "Auth Failed"; + + /** + * for prometheus task name prefix + */ + String PROMETHEUS_APP_PREFIX = "_prometheus_"; + + /** + * prometheus + */ + String PROMETHEUS = "prometheus"; + + /** + * status page component state normal + */ + byte STATUS_PAGE_COMPONENT_STATE_NORMAL = 0; + + /** + * status page component state abnormal + */ + byte STATUS_PAGE_COMPONENT_STATE_ABNORMAL = 1; + + /** + * status page component state unknown + */ + byte STATUS_PAGE_COMPONENT_STATE_UNKNOWN = 2; + + /** + * status page org state all normal + */ + byte STATUS_PAGE_ORG_STATE_ALL_NORMAL = 0; + + /** + * status page org state some abnormal + */ + byte STATUS_PAGE_ORG_STATE_SOME_ABNORMAL = 1; + + /** + * status page org state all abnormal + */ + byte STATUS_PAGE_ORG_STATE_ALL_ABNORMAL = 2; + + /** + * status page component calculate method auto + */ + byte STATUE_PAGE_CALCULATE_METHOD_AUTO = 0; + + /** + * status page component calculate method manual + */ + byte STATUS_PAGE_CALCULATE_METHOD_MANUAL = 1; + + /** + * status page incident state investigating + */ + byte STATUS_PAGE_INCIDENT_STATE_INVESTIGATING = 0; + + /** + * status page incident state identified + */ + byte STATUS_PAGE_INCIDENT_STATE_IDENTIFIED = 1; + + /** + * status page incident state monitoring + */ + byte STATUS_PAGE_INCIDENT_STATE_MONITORING = 2; + + /** + * status page incident state resolved + */ + byte STATUS_PAGE_INCIDENT_STATE_RESOLVED = 3; + + /** + * status page incident state resolved + */ + byte MONITOR_TYPE_NORMAL = 0; + + /** + * status page incident state resolved + */ + byte MONITOR_TYPE_PUSH_AUTO_CREATE = 1; + + /** + * status page incident state resolved + */ + byte MONITOR_TYPE_DISCOVERY_AUTO_CREATE = 2; + + /** + * JEXL custom function `json` + */ + String JEXL_CUSTOM_JSON_FUNCTION = "json"; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ConfigConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ConfigConstants.java new file mode 100644 index 0000000..47e8bac --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ConfigConstants.java @@ -0,0 +1,74 @@ +/* + * 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.constants; + +/** + * Extract all public strings that the project needs to configure in yml. + */ + +public interface ConfigConstants { + + /** + * System Constant: hertzbeat + */ + interface SystemConstant { + String PROJECT_NAME = "hertzbeat"; + } + + /** + * Package name constant. + */ + interface PkgConstant { + String PKG = "org.apache.hertzbeat"; + } + + /** + * hertzbeat project module constant. + */ + interface FunctionModuleConstants { + + String MANAGER = "manager"; + + String ALERT = "alert"; + + String ALERTER = "alerter"; + + String COLLECTOR = "collector"; + + String COMMON = "common"; + + String WAREHOUSE = "warehouse"; + + String AI = "ai"; + + String STATUS = "status"; + + String SCHEDULER = "scheduler"; + + String PUSH = "push"; + + String DISPATCH = "dispatch"; + + String INFO = "info"; + + String GRAFANA = "grafana"; + + String LOG = "log"; + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/DataQueueConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/DataQueueConstants.java new file mode 100644 index 0000000..c8e51b6 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/DataQueueConstants.java @@ -0,0 +1,54 @@ +/* + * 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.constants; + +/** + * Queue dependent constant + */ +public interface DataQueueConstants { + + /** + * Queue name prefix + */ + String PREFIX = "common.queue"; + + /** + * name + */ + String NAME = "type"; + + /** + * kafka + */ + String KAFKA = "kafka"; + + /** + * Local memory + */ + String IN_MEMORY = "memory"; + + /** + * redis + */ + String REDIS = "redis"; + + /** + * netty + */ + String NETTY = "netty"; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ExportFileConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ExportFileConstants.java new file mode 100644 index 0000000..cc800fd --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ExportFileConstants.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.constants; + +/** + * Export file type constants + */ + +public interface ExportFileConstants { + + /** + * Export file name constants. + */ + String FILE = "file"; + + /** + * Excel export files constants. + */ + interface ExcelFile { + + /** + * Export file type. + */ + String TYPE = "EXCEL"; + + /** + * Export file suffix. + */ + String FILE_SUFFIX = ".xlsx"; + } + + /** + * Json export file constants. + */ + interface JsonFile { + + /** + * Export file type. + */ + String TYPE = "JSON"; + + /** + * Export file suffix. + */ + String FILE_SUFFIX = ".json"; + } + + /** + * Yaml export file constants. + */ + interface YamlFile { + + /** + * Export file type. + */ + String TYPE = "YAML"; + + /** + * Export file suffix. + */ + String FILE_SUFFIX = ".yaml"; + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/GeneralConfigTypeEnum.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/GeneralConfigTypeEnum.java new file mode 100644 index 0000000..ce54a70 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/GeneralConfigTypeEnum.java @@ -0,0 +1,64 @@ +/* + * 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.constants; + +/** + * General Config Type Enum + */ +public enum GeneralConfigTypeEnum { + + /** + * mute config + */ + mute, + + /** + * template config + */ + template, + + /** + * system secret config + */ + secret, + + /** + * sms general config + */ + sms, + + /** + * system config + */ + system, + + /** + * mail general config + */ + email, + + /** + * system store config + */ + oss, + + /** + * model llm provider + */ + provider +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/HertzBeatKeywordsEnum.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/HertzBeatKeywordsEnum.java new file mode 100644 index 0000000..5f51de2 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/HertzBeatKeywordsEnum.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.constants; + +/** + * HertzBeat Reserved Keywords Enum + */ +public enum HertzBeatKeywordsEnum { + + INSTANCE("instance", "metric_instance"); + + private final String keyword; + + private final String alias; + + HertzBeatKeywordsEnum(String keyword, String alias) { + this.keyword = keyword; + this.alias = alias; + } + + public String getKeyword() { + return keyword; + } + + public String getAlias() { + return alias; + } +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ImExportTaskConstant.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ImExportTaskConstant.java new file mode 100644 index 0000000..4d8fb53 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ImExportTaskConstant.java @@ -0,0 +1,31 @@ +/* + * 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.constants; + +/** + * Import/Export task constants + */ + +public interface ImExportTaskConstant { + + /** + * If the number of tasks reaches this threshold, progress information will broadcast. + * Kept low so medium-sized imports (e.g. dozens of monitors) still report incremental progress. + */ + Integer IMPORT_TASK_PROCESS_THRESHOLD = 10; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ImportTaskStatusEnum.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ImportTaskStatusEnum.java new file mode 100644 index 0000000..c98de9e --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ImportTaskStatusEnum.java @@ -0,0 +1,49 @@ +/* + * 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.constants; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** + * Import/Export task status + */ +@Getter +@AllArgsConstructor +@ToString +public enum ImportTaskStatusEnum { + + /** + * In progress + */ + IN_PROGRESS("IN_PROGRESS"), + + /** + * Completed + */ + COMPLETED("COMPLETED"), + + /** + * Failed + */ + FAILED("FAILED"); + + private final String value; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/JexlKeywordsEnum.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/JexlKeywordsEnum.java new file mode 100644 index 0000000..13f4c9d --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/JexlKeywordsEnum.java @@ -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.common.constants; + +import java.util.Arrays; + +/** + * Jexl keywords enum + */ +public enum JexlKeywordsEnum { + + /** + * Official reserved keywords + * @reference https://commons.apache.org/proper/commons-jexl/reference/syntax.html + */ + OR("or"), + AND("and"), + EQ("eq"), + NE("ne"), + LT("lt"), + GT("gt"), + LE("le"), + GE("ge"), + DIV("div"), + MOD("mod"), + NOT("not"), + NULL("null"), + TRUE("true"), + FALSE("false"), + NEW("new"), + VAR("var"), + DO("do"), + WHILE("while"), + BREAK("break"), + CONTINUE("continue"), + FUNCTION("function"), + RETURN("return"), + + /** + * Syntax-reserved keywords + */ + IF("if"), + ELSE("else"), + FOR("for"), + + /** + * Built-in functions reserve keywords + */ + SIZE("size"), + EMPTY("empty"); + + + private final String keyword; + + JexlKeywordsEnum(String keyword) { + this.keyword = keyword; + } + + public String getKeyword() { + return keyword; + } + + public static boolean match(String word) { + if (word == null || word.trim().isEmpty()) { + return false; + } + return Arrays.stream(values()).anyMatch(t -> t.keyword.equals(word)); + } + +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ManagerEventTypeEnum.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ManagerEventTypeEnum.java new file mode 100644 index 0000000..687fbb6 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ManagerEventTypeEnum.java @@ -0,0 +1,39 @@ +/* + * 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.constants; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** + * Manager Event Type Enum + */ +@Getter +@AllArgsConstructor +@ToString +public enum ManagerEventTypeEnum { + + /** + * IMPORT_TASK_EVENT + */ + IMPORT_TASK_EVENT("IMPORT_TASK_EVENT"); + + private final String value; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/MetricDataConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/MetricDataConstants.java new file mode 100644 index 0000000..b73d3ad --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/MetricDataConstants.java @@ -0,0 +1,42 @@ +/* + * 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.constants; + +/** + * Metric Data Field Constants + */ +public interface MetricDataConstants { + // key in Field + String TYPE = "type"; + String LABEL = "label"; + String UNIT = "unit"; + + // key in Schema metadata + String ID = "id"; + String TENANT_ID = "tenantId"; + String APP = "app"; + String METRICS = "metrics"; + String LABELS = "labels"; + String ANNOTATIONS = "annotations"; + String PRIORITY = "priority"; + String TIME = "time"; + String CODE = "code"; + String MSG = "msg"; + String INSTANCE_NAME = "instancename"; + String INSTANCE = "instance"; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/NetworkConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/NetworkConstants.java new file mode 100644 index 0000000..7c6d15e --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/NetworkConstants.java @@ -0,0 +1,66 @@ +/* + * 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.constants; + +/** + * Http Constants. + */ + +public interface NetworkConstants { + + String KEEP_ALIVE = "Keep-Alive"; + + String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36"; + + String IPV6 = "ipv6"; + + String IPV4 = "ipv4"; + + String ERROR_MSG = "errorMsg"; + + String URL = "url"; + + String HTTP_HEADER = "http://"; + + String HTTPS_HEADER = "https://"; + + String RESPONSE_TIME = "responseTime"; + + String STATUS_CODE = "StatusCode"; + + String X_AUTH_TOKEN = "X-Auth-Token"; + + String LOCATION = "Location"; + + String BASIC = "Basic"; + + String AUTHORIZATION = "Authorization"; + + /** + * HttpClient Configuration Constants. + */ + interface HttpClientConstants { + + int READ_TIME_OUT = 6 * 1000; + int WRITE_TIME_OUT = 6 * 1000; + int CONNECT_TIME_OUT = 6 * 1000; + int MAX_IDLE_CONNECTIONS = 20; + int KEEP_ALIVE_TIMEOUT = 30 * 1000; + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/NotifyLevelEnum.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/NotifyLevelEnum.java new file mode 100644 index 0000000..ab97a4b --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/NotifyLevelEnum.java @@ -0,0 +1,40 @@ +/* + * 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.constants; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** + * Notify Level Enum + */ +@Getter +@AllArgsConstructor +@ToString +public enum NotifyLevelEnum { + + SUCCESS("SUCCESS"), + ERROR("ERROR"), + INFO("INFO"), + WARNING("WARNING"), + BLANK("BLANK"); + + private final String value; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/PluginType.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/PluginType.java new file mode 100644 index 0000000..3b94797 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/PluginType.java @@ -0,0 +1,33 @@ +/* + * 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.constants; + +/** + * plugin type + */ +public enum PluginType { + + /** + * do something after alter + */ + POST_ALERT, + /** + * do something after collect + */ + POST_COLLECT +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/SignConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/SignConstants.java new file mode 100644 index 0000000..b69e1e1 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/SignConstants.java @@ -0,0 +1,42 @@ +/* + * 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.constants; + +/** + * Sign Constants + */ +public interface SignConstants { + + String DOUBLE_MARK = ":"; + + String SINGLE_MARK = ";"; + + String WELL_NO = "#"; + + String LINE_FEED = "\n"; + + String CARRIAGE_RETURN = "\r"; + + String RIGHT_DASH = "/"; + + String COMMA = ","; + + String BLANK = " "; + + String DOT = "."; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/SmsConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/SmsConstants.java new file mode 100644 index 0000000..7929035 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/SmsConstants.java @@ -0,0 +1,41 @@ +/* + * 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.constants; + +/** + * SMS provider constants + */ +public interface SmsConstants { + // Tencent cloud SMS + String TENCENT = "tencent"; + + // Alibaba Cloud SMS + String ALIBABA = "alibaba"; + + // UniSMS + String UNISMS = "unisms"; + + // Smslocal SMS + String SMSLOCAL = "smslocal"; + + // Aws cloud SMS + String AWS = "aws"; + + // Twilio SMS + String TWILIO = "twilio"; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ThemeEnum.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ThemeEnum.java new file mode 100644 index 0000000..2138ea2 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ThemeEnum.java @@ -0,0 +1,50 @@ +/* + * 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.constants; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** + * Theme Enum + */ +@Getter +@AllArgsConstructor +@ToString +public enum ThemeEnum { + + COMPACT("compact"), DEFAULT("default"), LIGHT("light"), DARK("dark"); + + private final String value; + + /** + * Convert theme to standard theme + * + * @param theme the input theme + * @return the converted theme + */ + public static String convert(String theme) { + if ((theme == null || theme.isEmpty()) + || ThemeEnum.COMPACT.getValue().equals(theme) + || ThemeEnum.DEFAULT.getValue().equals(theme)) { + return ThemeEnum.LIGHT.getValue(); + } + return theme; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/arrow/ArrowCell.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/arrow/ArrowCell.java new file mode 100644 index 0000000..59f5b84 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/arrow/ArrowCell.java @@ -0,0 +1,39 @@ +/* + * 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.arrow; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.arrow.vector.table.Row; +import org.apache.arrow.vector.types.pojo.Field; + +/** + * A cell consisting of {@link Field} and value + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArrowCell extends DefaultMetadataReader { + private final String value; + private final Field field; + + public ArrowCell(Field field, Row row) { + this.field = field; + this.value = row.getVarCharObj(field.getName()); + this.metadata = field.getMetadata(); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/arrow/DefaultMetadataReader.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/arrow/DefaultMetadataReader.java new file mode 100644 index 0000000..2a02d31 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/arrow/DefaultMetadataReader.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.arrow; + +import java.util.Map; + +/** + * implementation of MetadataOperator + */ +public class DefaultMetadataReader implements MetadataReader { + protected Map<String, String> metadata; + + @Override + public String getMetadataAsString(String key) { + return metadata.get(key); + } + + @Override + public Boolean getMetadataAsBoolean(String key) { + return Boolean.parseBoolean(metadata.get(key)); + } + + @Override + public Byte getMetadataAsByte(String key) { + return Byte.parseByte(metadata.get(key)); + } + + @Override + public Integer getMetadataAsInteger(String key) { + return Integer.parseInt(metadata.get(key)); + } + + @Override + public Long getMetadataAsLong(String key) { + return Long.parseLong(metadata.get(key)); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/arrow/MetadataReader.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/arrow/MetadataReader.java new file mode 100644 index 0000000..d000a98 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/arrow/MetadataReader.java @@ -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. + */ + +package org.apache.hertzbeat.common.entity.arrow; + +/** + */ +public interface MetadataReader { + String getMetadataAsString(String key); + + Boolean getMetadataAsBoolean(String key); + + Byte getMetadataAsByte(String key); + + Integer getMetadataAsInteger(String key); + + Long getMetadataAsLong(String key); +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/arrow/RowWrapper.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/arrow/RowWrapper.java new file mode 100644 index 0000000..0ce119c --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/arrow/RowWrapper.java @@ -0,0 +1,82 @@ +/* + * 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.arrow; + +import lombok.Getter; +import org.apache.arrow.vector.table.Row; +import org.apache.arrow.vector.types.pojo.Field; + +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Stream; + +/** + * A wrapper of row, which contains row info, fields info in this row and each cell of field. + * When using RowWrapper, it is necessary to call {@link RowWrapper#hasNextRow()} and {@link RowWrapper#nextRow()} + */ +public class RowWrapper { + private final Row currentRow; + private final Iterator<Row> rowIterator; + @Getter + private final List<Field> fieldList; + @Getter + private int rowIndex; + private int fieldIndex; + + public RowWrapper(Row row, Iterator<Row> rowIterator, List<Field> fieldList, int rowIndex) { + this.currentRow = row; + this.rowIterator = rowIterator; + this.fieldList = fieldList; + this.fieldIndex = 0; + this.rowIndex = rowIndex; + } + + public boolean hasNextRow() { + return rowIterator.hasNext(); + } + + public RowWrapper nextRow() { + return new RowWrapper(rowIterator.next(), rowIterator, fieldList, ++rowIndex); + } + + /** + * <p>Get next cell in current row. + * <p>RowWrapper maintain a field index internally, which means cannot back to previous cell after call this method. + * However, you can use {@link RowWrapper#resetCellIndex()} to reset field index to the beginning. + * @return ArrowCell + */ + public ArrowCell nextCell() { + if (!hasNextCell() || currentRow == null) { + throw new NoSuchElementException("No more cells in current row"); + } + return new ArrowCell(fieldList.get(fieldIndex++), currentRow); + } + + public Stream<ArrowCell> cellStream() { + return fieldList.stream().map(field -> new ArrowCell(field, currentRow)); + } + + public boolean hasNextCell() { + return fieldIndex < fieldList.size(); + } + + public void resetCellIndex() { + this.fieldIndex = 0; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/AlertReport.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/AlertReport.java new file mode 100644 index 0000000..34239d1 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/AlertReport.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Alarm Report Content Entity + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Alarm Report Content Entity") +public class AlertReport { + + @Schema(title = "Alert record saas index ID") + private String alertId; + + @Schema(title = "Alert Name") + private String alertName; + + @Schema(title = "Alarm evaluation interval") + private Integer alertDuration; + + @Schema(title = "Time when the log service receives the alarm message", + description = "Time when the log service receives the alarm message", + example = "1694589491000", accessMode = READ_WRITE) + private long alertTime; + + @Schema(title = "Alarm priority. " + + "0: high emergency alarm red " + + "1: medium critical serious alarm Orange " + + "2: low warning warning alarm yellow", + description = "Alarm priority. 0: high emergency alarm red " + + "1: medium critical serious alarm Orange " + + "2: low warning warning alarm yellow", + example = "1", accessMode = READ_WRITE) + private Integer priority; + + @Schema(title = "Alarm type. 0: the default 1 is business system exception reporting", + description = "Alarm type. 0: the default 1 is business system exception reporting", + example = "0", accessMode = READ_WRITE) + private Integer reportType; + + @Schema(title = "Alarm tag information", + description = "Alarm label information((monitorId:xxx,monitorName:xxx))", + example = "{\"key1\":\"value1\"}", accessMode = READ_WRITE) + private Map<String, String> labels; + + @Schema(title = " Alarm marking (monitorId:xxx,monitorName:xxx)", + description = "Alarm labeling", + example = "{\"key1\":\"value1\"}" + , accessMode = READ_WRITE) + private Map<String, String> annotations; + + @Schema(title = " Alarm content", + description = "Content of alarm", + example = "External alarm content", + accessMode = READ_WRITE) + private String content; + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/CollectorInfo.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/CollectorInfo.java new file mode 100644 index 0000000..7c5a889 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/CollectorInfo.java @@ -0,0 +1,47 @@ +/* + * 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; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.constants.CommonConstants; + +/** + * collector info + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "collector info") +public class CollectorInfo { + + @NotNull + private String name; + + private String ip; + + private String version; + + @NotNull + private String mode = CommonConstants.MODE_PUBLIC; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/Field.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/Field.java new file mode 100644 index 0000000..72c7ccd --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/Field.java @@ -0,0 +1,48 @@ +/* + * 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; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * monitoring metrics field + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Monitoring Metrics Field") +public class Field { + + @Schema(title = "Metrics Field Name") + private String name; + + @Schema(title = "Field Type:0-number 1-string") + private Byte type; + + @Schema(title = "Field Unit") + private String unit; + + @Schema(title = "Whether is a label") + private Boolean label; + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ImportTaskMessage.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ImportTaskMessage.java new file mode 100644 index 0000000..85e71b3 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ImportTaskMessage.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.dto; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.hertzbeat.common.constants.ImportTaskStatusEnum; +import org.apache.hertzbeat.common.constants.ManagerEventTypeEnum; +import org.apache.hertzbeat.common.constants.NotifyLevelEnum; + +/** + * Import task message + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class ImportTaskMessage extends ManagerMessage { + + /** + * Task name + */ + @NotNull + private String taskName; + + /** + * Progress, expressed as a percentage + */ + @Nullable + private Integer progress; + + /** + * Task Status, + * @see ImportTaskStatusEnum + */ + @NotNull + private String status; + + /** + * If Fail, the error message + */ + @Nullable + private String errMsg; + + public ImportTaskMessage(String notifyLevel, String managerEventType, String taskName, @Nullable Integer progress, String status, @Nullable String errMsg){ + super(notifyLevel, managerEventType); + this.taskName = taskName; + this.progress = progress; + this.status = status; + this.errMsg = errMsg; + } + + public static ManagerMessage createInProgressMessage(String taskName, Integer process){ + return new ImportTaskMessage(NotifyLevelEnum.INFO.getValue(), ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), taskName, process, ImportTaskStatusEnum.IN_PROGRESS.getValue(), null); + } + + public static ManagerMessage createCompletedMessage(String taskName){ + return new ImportTaskMessage(NotifyLevelEnum.SUCCESS.getValue(), ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), taskName, null, ImportTaskStatusEnum.COMPLETED.getValue(), null); + } + + public static ManagerMessage createFailedMessage(String taskName, String errMsg){ + return new ImportTaskMessage(NotifyLevelEnum.ERROR.getValue(), ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), taskName, null, ImportTaskStatusEnum.FAILED.getValue(), errMsg); + } +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/MailServerConfig.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/MailServerConfig.java new file mode 100644 index 0000000..3c99b34 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/MailServerConfig.java @@ -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.common.entity.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Email account configuration dto + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MailServerConfig { + + @NotNull(message = "Type cannot be empty") + private Integer type; + + @NotBlank(message = "Mail host cannot be empty") + private String emailHost; + + @NotBlank(message = "Username cannot be empty") + @Email + private String emailUsername; + + @NotBlank(message = "Password cannot be empty") + private String emailPassword; + + @NotNull(message = "Mail port cannot be null") + @Max(message = "Mail port must be less than or equal to 65535", value = 65535) + @Min(message = "Mail port must be greater than or equal to 1", value = 1) + private Integer emailPort; + + private boolean emailSsl = true; + + private boolean emailStarttls = false; + + private boolean enable = true; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ManagerMessage.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ManagerMessage.java new file mode 100644 index 0000000..47d87de --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ManagerMessage.java @@ -0,0 +1,42 @@ +/* + * 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; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Manager Message Entity + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ManagerMessage { + /** + * Notify Level + * @see org.apache.hertzbeat.common.constants.NotifyLevelEnum + */ + private String notifyLevel; + + /** + * Manager Event Type + * @see org.apache.hertzbeat.common.constants.ManagerEventTypeEnum + */ + private String managerEventType; +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/Message.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/Message.java new file mode 100644 index 0000000..5505b7a --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/Message.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.apache.hertzbeat.common.constants.CommonConstants; + +/** + * Unified message structure definition for front and back ends + * <p> + * { + * data:{....}, + * msg: message, + * code: 3432 + * } + */ +@Data +@Schema(description = "Common message structure") +public class Message<T> { + + /** + * message body data + */ + @Schema(description = "Response Data") + private T data; + + /** + * exception message when error happen or success message + */ + @Schema(title = "Other Message") + private String msg; + + /** + * response code, not http code + */ + @Schema(title = "Response Code") + private byte code = CommonConstants.SUCCESS_CODE; + + public static <T> Message<T> success() { + return new Message<>(); + } + + public static <T> Message<T> success(String msg) { + return new Message<>(msg); + } + + public static <T> Message<T> fail(byte code, String msg) { + return new Message<>(code, msg); + } + + public static <T> Message<T> success(T data) { + return new Message<>(data); + } + + public static <T> Message<T> successWithData(T data) { + return new Message<>(data); + } + + private Message() { + } + + private Message(String msg) { + this.msg = msg; + } + + private Message(byte code, String msg) { + this.code = code; + this.msg = msg; + } + + private Message(T data) { + this.data = data; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/MetricsData.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/MetricsData.java new file mode 100644 index 0000000..f7085f2 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/MetricsData.java @@ -0,0 +1,54 @@ +/* + * 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; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Monitoring Metrics Data + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Monitoring Metrics Data") +public class MetricsData { + + @Schema(title = "Monitoring Task ID") + private Long id; + + @Schema(title = "Monitoring Type") + private String app; + + @Schema(title = "Monitoring Metrics") + private String metrics; + + @Schema(title = "Latest Collect Time") + private Long time; + + @Schema(description = "Monitoring Metrics fields") + private List<Field> fields; + + @Schema(description = "Monitoring Metrics DataRow") + private List<ValueRow> valueRows; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/MetricsHistoryData.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/MetricsHistoryData.java new file mode 100644 index 0000000..498fa7b --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/MetricsHistoryData.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Metric History Range Query Data + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Metric History Range Query Data") +public class MetricsHistoryData { + + @Schema(title = "Monitor Instance (e.g., ip:port or domain)") + private String instance; + + @Schema(title = "Monitoring Type") + private String app; + + @Schema(title = "Monitoring Metrics") + private String metrics; + + @Schema(title = "Monitoring Metrics Field") + private Field field; + + @Schema(description = "Monitoring Range Query Data tags<==>values") + private Map<String, List<Value>> values; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ModelProviderConfig.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ModelProviderConfig.java new file mode 100644 index 0000000..5aaa2d7 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ModelProviderConfig.java @@ -0,0 +1,54 @@ +/* + * 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; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Model Provider Configuration + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "LLM Model Provider configuration") +public class ModelProviderConfig { + + @Schema(title = "Model type, text-generate, vision") + private String type; + + @Schema(title = "Model Provider code, like openai, zai, bigmodel") + private String code; + + @Schema(title = "custom the provider server base url") + private String baseUrl; + + @Schema(title = "use the model id name, eg: gpt-5, glm-4.6") + private String model; + + @Schema(title = "API Key", description = "API key", example = "sk-...") + @NotBlank(message = "API Key cannot be empty when enabled") + private String apiKey; + + @Schema(title = "Participation Model", description = "model for participation", example = "PROTECTED") + @NotBlank(message = "API Key cannot be empty when enabled") + private String participationModel; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/PromVectorOrMatrix.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/PromVectorOrMatrix.java new file mode 100644 index 0000000..be1ff70 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/PromVectorOrMatrix.java @@ -0,0 +1,100 @@ +/* + * 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; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.gson.JsonObject; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.Accessors; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.annotation.JsonDeserialize; + +/** + * prometheus vector or matrix entity + */ +@lombok.Data +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = true) +@ToString +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +public class PromVectorOrMatrix { + private String status; + private Data data; + + /** + * PromVectorOrMatrix.Data + */ + @lombok.Data + @AllArgsConstructor + @NoArgsConstructor + @Accessors(chain = true) + @ToString + public static class Data { + String resultType; + List<Result> result; + } + + /** + * PromVectorOrMatrix.Result + */ + @lombok.Data + @AllArgsConstructor + @NoArgsConstructor + @Accessors(chain = true) + @ToString + public static class Result { + @JsonDeserialize(using = MetricJsonObjectDeserializer.class) + JsonObject metric; + List<Object> value; + List<List<Object>> values; + } + + /** + * MetricJsonObjectDeserializer + */ + @EqualsAndHashCode(callSuper = true) + @lombok.Data + @NoArgsConstructor + @Accessors(chain = true) + public static class MetricJsonObjectDeserializer extends ValueDeserializer<JsonObject> { + + @Override + public JsonObject deserialize(JsonParser jp, DeserializationContext ctxt) { + JsonNode node = jp.objectReadContext().readTree(jp); + + JsonObject metric = new JsonObject(); + if (node != null && node.isObject()) { + node.properties().forEach(entry -> + metric.addProperty(entry.getKey(), entry.getValue().asString()) + ); + } + + return metric; + } + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ServerInfo.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ServerInfo.java new file mode 100644 index 0000000..4b3fe8a --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ServerInfo.java @@ -0,0 +1,40 @@ +/* + * 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; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * collector info + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "manager server info") +public class ServerInfo { + + /** + * the aes secret key value for aes encode decode + */ + private String aesSecret; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/Value.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/Value.java new file mode 100644 index 0000000..6898fde --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/Value.java @@ -0,0 +1,62 @@ +/* + * 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; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Monitoring Metrics Data + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Monitoring Metrics Data") +public class Value { + + public Value(String origin) { + this.origin = origin; + } + + public Value(String origin, long time) { + this.origin = origin; + this.time = time; + } + + @Schema(title = "Origin Value") + private String origin; + + @Schema(title = "Mean Avg Value") + private String mean; + + @Schema(title = "Median Value, Not Support Now") + private String median; + + @Schema(title = "Min Value") + private String min; + + @Schema(title = "Max Value") + private String max; + + @Schema(title = "Collect Data Time(effect when history range data)") + private Long time; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ValueRow.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ValueRow.java new file mode 100644 index 0000000..340b45a --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/ValueRow.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Monitoring Metrics DataRow + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Monitoring Metrics DataRow") +public class ValueRow { + + @Schema(title = "Value Row Labels") + private Map<String, String> labels; + + @Schema(description = "Monitoring Metrics DataRow") + private List<Value> values; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/query/DatasourceQuery.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/query/DatasourceQuery.java new file mode 100644 index 0000000..1f97f46 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/query/DatasourceQuery.java @@ -0,0 +1,59 @@ +/* + * 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.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Metric History Range Query Data + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Query Request Data") +public class DatasourceQuery { + + @Schema(title = "Ref Id, unique id for the query") + private String refId; + + @Schema(title = "datasource name") + private String datasource; + + @Schema(title = "query expr, like prometheus query") + private String expr; + + @Schema(title = "query expr type, like promql or sql or influxql") + private String exprType; + + @Schema(title = "query range type, like range or instant") + private String timeType; + + @Schema(title = "query range start time") + private Long start; + + @Schema(title = "query range end time") + private Long end; + + @Schema(title = "query time step, like 5m or 1h") + private String step; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/query/DatasourceQueryData.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/query/DatasourceQueryData.java new file mode 100644 index 0000000..8727152 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/query/DatasourceQueryData.java @@ -0,0 +1,103 @@ +/* + * 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.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Metric History Range Query Data + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Metric Query Data") +public class DatasourceQueryData { + + @Schema(title = "Ref Id, unique id for the query") + private String refId; + + @Schema(title = "query status code, 200 for success, other for error") + private Integer status; + + @Schema(title = "query error message") + private String msg; + + @Schema(title = "query result data frames") + private List<SchemaData> frames; + + /** + * Schema Data + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + public static final class SchemaData { + + @Schema(title = "Metric Schema") + private MetricSchema schema; + + @Schema(title = "metrics row values, first is the timestamp-ts", example = "[[29,32,44],[32,34,true]]") + private List<Object[]> data; + } + + /** + * Metric Schema + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + public static final class MetricSchema { + + @Schema(title = "Metrics Field") + private List<MetricField> fields; + + @Schema(title = "This frame labels") + private Map<String, String> labels; + + @Schema(title = "Meta Information") + private Map<String, String> meta; + } + + /** + * Metric Field + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + public static final class MetricField { + + @Schema(title = "Metric Field Name") + private String name; + + @Schema(title = "Field Type: number, string, time, bool") + private String type; + + @Schema(title = "Field Unit: %, Mb, Kbps etc.") + private String unit; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/AlibabaSmsProperties.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/AlibabaSmsProperties.java new file mode 100644 index 0000000..4de833f --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/AlibabaSmsProperties.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.dto.sms; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Alibaba Cloud SMS configuration properties + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AlibabaSmsProperties { + /** + * Alibaba Cloud access key id + */ + @NotBlank(message = "AccessKeyId cannot be empty") + private String accessKeyId; + + /** + * Alibaba Cloud access key secret + */ + @NotBlank(message = "AccessKeySecret cannot be empty") + private String accessKeySecret; + + /** + * SMS signature + */ + @NotBlank(message = "SignName cannot be empty") + private String signName; + + /** + * SMS template Code + */ + @NotBlank(message = "TemplateCode cannot be null") + private String templateCode; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/AwsSmsProperties.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/AwsSmsProperties.java new file mode 100644 index 0000000..6e04087 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/AwsSmsProperties.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.dto.sms; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * AWS Cloud SMS configuration properties + */ +@Data +public class AwsSmsProperties { + /** + * AWS Cloud access key id + */ + @NotBlank(message = "AccessKeyId cannot be empty") + private String accessKeyId; + + /** + * AWS Cloud access key secret + */ + @NotBlank(message = "AccessKeySecret cannot be empty") + private String accessKeySecret; + + /** + * AWS Cloud region + */ + @NotBlank(message = "region cannot be empty") + private String region; + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/SmsConfig.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/SmsConfig.java new file mode 100644 index 0000000..c689b91 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/SmsConfig.java @@ -0,0 +1,73 @@ +/* + * 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.sms; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Framework-agnostic SMS runtime configuration. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SmsConfig { + + /** + * whether to enable SMS, default is false + */ + private boolean enable = false; + + /** + * sms service provider + */ + @NotBlank(message = "Type cannot be empty") + private String type; + + /** + * Tencent cloud SMS configuration + */ + private TencentSmsProperties tencent; + + /** + * Aliyun SMS configuration + */ + private AlibabaSmsProperties alibaba; + + /** + * UniSMS configuration + */ + private UniSmsProperties unisms; + + /** + * Aws configuration + */ + private AwsSmsProperties aws; + + /** + * Twilio SMS configuration + */ + private TwilioSmsProperties twilio; + + /** + * Smslocal SMS configuration + */ + private SmslocalSmsProperties smslocal; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/SmslocalSmsProperties.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/SmslocalSmsProperties.java new file mode 100644 index 0000000..3d39d5f --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/SmslocalSmsProperties.java @@ -0,0 +1,37 @@ +/* + * 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.sms; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Smslocal SMS Properties + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SmslocalSmsProperties { + /** + * SmsLocal account api key + */ + private String apiKey; + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/TencentSmsProperties.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/TencentSmsProperties.java new file mode 100644 index 0000000..7394dc0 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/TencentSmsProperties.java @@ -0,0 +1,61 @@ +/* + * 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.sms; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Tencent Cloud SMS properties + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TencentSmsProperties { + /** + * Tencent cloud account secret id + */ + @NotBlank(message = "SecretId cannot be empty") + private String secretId; + + /** + * Tencent cloud account secret key + */ + @NotBlank(message = "SecretKey cannot be empty") + private String secretKey; + + /** + * SMS app id + */ + @NotBlank(message = "AppId cannot be null") + private String appId; + + /** + * SMS signature + */ + @NotBlank(message = "SignName cannot be empty") + private String signName; + + /** + * SMS template ID + */ + @NotBlank(message = "templateId cannot be null") + private String templateId; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/TwilioSmsProperties.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/TwilioSmsProperties.java new file mode 100644 index 0000000..57013ad --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/TwilioSmsProperties.java @@ -0,0 +1,49 @@ +/* + * 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.sms; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Twilio SMS configuration properties + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TwilioSmsProperties { + /** + * Twilio Account SID + */ + @NotBlank(message = "Account SID cannot be empty") + private String accountSid; + + /** + * Twilio Auth Token + */ + @NotBlank(message = "Auth Token cannot be empty") + private String authToken; + + /** + * Twilio Issued Phone Number + */ + @NotBlank(message = "Twilio Phone Number cannot be empty") + private String twilioPhoneNumber; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/UniSmsProperties.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/UniSmsProperties.java new file mode 100644 index 0000000..ea32e4b --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/sms/UniSmsProperties.java @@ -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.common.entity.dto.sms; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * UniSMS properties + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UniSmsProperties { + /** + * UniSMS access key id + */ + @NotBlank(message = "accessKeyId cannot be empty") + private String accessKeyId; + + /** + * UniSMS access key secret, required for HMAC mode + */ + private String accessKeySecret; + + /** + * SMS signature + */ + @NotBlank(message = "signature cannot be null") + private String signature; + + /** + * SMS template ID + */ + @NotBlank(message = "templateId cannot be null") + private String templateId; + + /** + * Authentication mode: simple or hmac, default is simple + */ + @NotBlank(message = "authMode cannot be null") + private String authMode = "simple"; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Configmap.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Configmap.java new file mode 100644 index 0000000..f930931 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Configmap.java @@ -0,0 +1,54 @@ +/* + * 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.job; + +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Monitoring configuration parameter properties and values + * During the process, you need to replace the content with the identifier ^_^key^_^ + * in the protocol configuration parameter with the real value in the configuration parameter + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Configmap implements Serializable { + + /** + * Parameter key, replace the content with the identifier ^^_key_^^ in the protocol + * configuration parameter with the real value in the configuration parameter + */ + private String key; + + /** + * parameter value + */ + private Object value; + + /** + * Parameter type + * 0: number 1: string 2: encrypted string 3: json string mapped by map + * number,string,secret + */ + private byte type = 1; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java new file mode 100644 index 0000000..e7fb6e5 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java @@ -0,0 +1,375 @@ +/* + * 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.job; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.JsonUtil; + +/** + * Collect task details. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Slf4j +public class Job { + + /** + * Task Job id. + */ + private long id; + /** + * Tenant id. + */ + private long tenantId = 0; + /** + * Monitoring Task ID. + */ + private long monitorId; + /** + * metadata info bind with this job + * eg: instancename, instancehost + */ + private Map<String, String> metadata; + /** + * bind labels. + */ + private Map<String, String> labels; + /** + * bind annotations. + */ + private Map<String, String> annotations; + /** + * Is hide this app in main menus layout, only for app type, default true. + */ + private boolean hide = true; + /** + * Large categories of monitoring + * service-application service monitoring db-database + * monitoring custom-custom monitoring os-operating system monitoring... + */ + private String category; + /** + * Type of monitoring eg: linux | mysql | jvm. + */ + private String app; + /** + * The internationalized name of the monitoring type + * PING CONNECT. + */ + private Map<String, String> name; + /** + * The description and help of the monitoring type + * PING CONNECT - You can use the IP address or + * domain address of the peer service to monitor the PING connectivity between the local network and the peer network. + */ + private Map<String, String> help; + /** + * The monitor help link. + */ + private Map<String, String> helpLink; + /** + * Task dispatch start timestamp. + */ + private long timestamp; + /** + * Default task collection time interval (unit: second) eg: 30,60,600. + */ + private long defaultInterval = 600L; + /** + * Refresh time list for one cycle of the job. + */ + private ConcurrentLinkedDeque<Long> intervals; + /** + * Whether it is a recurring periodic task true is yes, false is no. + */ + private boolean isCyclic = false; + /** + * monitor input need params. + */ + private List<RuntimeParamDefine> params; + /** + * Metrics configuration eg: cpu memory. + */ + private List<Metrics> metrics; + /** + * Monitoring configuration parameter properties and values eg: username password timeout host. + */ + private List<Configmap> configmap; + /** + * Whether it is a service discovery job, true is yes, false is no. + */ + private boolean isSd = false; + + /** + * Whether to use the Prometheus proxy. + */ + private boolean prometheusProxyMode = false; + + /** + * Scheduling type: interval or cron. + */ + private String scheduleType = "interval"; + + /** + * Cron expression for scheduling, used when scheduleType is "cron". + */ + private String cronExpression = null; + + /** + * the collect data response metrics as env configmap for other collect use. ^o^xxx^o^ + */ + @JsonIgnore + private Map<String, Configmap> envConfigmaps; + + /** + * collector use - timestamp when the task was scheduled by the time wheel. + */ + @JsonIgnore + private transient long dispatchTime; + + /** + * collector usage - metric group task execution priority view + * 0 - availability + * 1 - cpu | memory + * 2 - health + * 3 - otherMetrics + * .... + * 126 - otherMetrics + * 127 - lastPriorMetrics + */ + @JsonIgnore + private transient LinkedList<Set<Metrics>> priorMetrics; + + /** + * collector use - Temporarily store one-time task metrics response data. + */ + @JsonIgnore + private transient List<CollectRep.MetricsData> responseDataTemp; + + /** + * collector use - construct to initialize metrics execution view. + */ + public synchronized void constructPriorMetrics() { + long now = System.currentTimeMillis(); + Map<Byte, List<Metrics>> currentCollectMetrics = metrics.stream() + .filter(metrics -> (now >= metrics.getCollectTime() + metrics.getInterval() * 1000)) + .peek(metric -> { + metric.setCollectTime(now); + // Determine whether to configure aliasFields If not, configure the default + if ((metric.getAliasFields() == null || metric.getAliasFields().isEmpty()) && metric.getFields() != null) { + metric.setAliasFields(metric.getFields().stream().map(Metrics.Field::getField).collect(Collectors.toList())); + } + // Set the default metrics execution priority, if not filled, the default last priority + if (metric.getPriority() == null) { + metric.setPriority(Byte.MAX_VALUE); + } + }) + .collect(Collectors.groupingBy(Metrics::getPriority)); + // the current collect metrics can not empty, if empty, add a default availability metrics + // due the metric collect is trigger by the previous metric collect + if (currentCollectMetrics.isEmpty()) { + Optional<Metrics> defaultMetricOption = metrics.stream().filter(metric -> metric.getPriority() == 0).findFirst(); + if (defaultMetricOption.isPresent()) { + Metrics defaultMetric = defaultMetricOption.get(); + defaultMetric.setCollectTime(now); + currentCollectMetrics.put((byte) 0, Collections.singletonList(defaultMetric)); + } else { + log.error("metrics must has one priority 0 metrics at least."); + } + } + // Construct a linked list of task execution order of the metrics + priorMetrics = new LinkedList<>(); + currentCollectMetrics.values().forEach(metric -> { + Set<Metrics> metricsSet = Collections.synchronizedSet(new HashSet<>(metric)); + priorMetrics.add(metricsSet); + }); + priorMetrics.sort(Comparator.comparing(e -> { + Optional<Metrics> metric = e.stream().findAny(); + if (metric.isPresent()) { + return metric.get().getPriority(); + } + return Byte.MAX_VALUE; + })); + envConfigmaps = new HashMap<>(8); + } + + /** + * collector use - to get the next set of priority metric group tasks. + * + * @param metrics Current Metrics + * @param first Is it the first time to get + * @return Metrics Tasks + * Returning null means: the job has been completed, and the collection of all metrics has ended + * Returning the empty set metrics that there are still metrics collection tasks at the current + * level that have not been completed,and the next level metrics task collection cannot be performed. + * The set returned empty means that there are still indicator collection tasks unfinished at the current level, + * and the task collection at the next level cannot be carried out + * Returns a set of data representation: get the next set of priority index collcet tasks + */ + public synchronized Set<Metrics> getNextCollectMetrics(Metrics metrics, boolean first) { + if (priorMetrics == null || priorMetrics.isEmpty()) { + return null; + } + Set<Metrics> metricsSet = priorMetrics.peek(); + if (first) { + if (metricsSet.isEmpty()) { + log.error("metrics must has one [availability] metrics at least."); + } + return metricsSet; + } + if (metrics == null) { + log.error("metrics can not null when not first get"); + return null; + } + if (!metricsSet.remove(metrics)) { + log.warn("Job {} appId {} app {} metrics {} remove empty error in priorMetrics.", + id, monitorId, app, metrics.getName()); + } + if (metricsSet.isEmpty()) { + priorMetrics.poll(); + if (priorMetrics.isEmpty()) { + return null; + } + Set<Metrics> source = priorMetrics.peek(); + return new HashSet<>(source); + } + return Collections.emptySet(); + } + + public void addCollectMetricsData(CollectRep.MetricsData metricsData) { + if (responseDataTemp == null) { + responseDataTemp = new LinkedList<>(); + } + responseDataTemp.add(metricsData); + } + + public Map<String, Configmap> getEnvConfigmaps() { + return envConfigmaps; + } + + public void addEnvConfigmaps(Map<String, Configmap> envConfigmaps) { + if (this.envConfigmaps == null) { + this.envConfigmaps = envConfigmaps; + } else { + this.envConfigmaps.putAll(envConfigmaps); + } + } + + @Override + public Job clone() { + // deep clone + return JsonUtil.fromJson(JsonUtil.toJson(this), getClass()); + } + + public void initIntervals() { + List<Long> metricsIntervals = new LinkedList<>(); + for (Metrics metrics: getMetrics()) { + metrics.setCollectTime(0L); + if (metrics.getInterval() <= 0) { + metrics.setInterval(defaultInterval); + } + if (!metricsIntervals.contains(metrics.getInterval())) { + metricsIntervals.add(metrics.getInterval()); + } + } + generateMetricsIntervals(metricsIntervals); + } + + /** + * The greatest common divisor. + */ + public static long gcd(long a, long b) { + while (b != 0) { + long temp = b; + b = a % b; + a = temp; + } + return a; + } + + /** + * The least common multiple. + */ + public static long lcm(List<Long> array) { + if (array != null && !array.isEmpty()) { + long result = array.get(0); + for (int i = 1; i < array.size(); i++) { + result = result / gcd(result, array.get(i)) * array.get(i); + } + return result; + } + return 0; + } + + /** + * @param metricsIntervals A unique list composed of intervals for all metrics + * Generate a list of refresh intervals for metric collection. + */ + public synchronized void generateMetricsIntervals(List<Long> metricsIntervals) { + // 1. To find the least common multiple (LCM) of all metric refresh intervals + long lcm = lcm(metricsIntervals); + List<Long> refreshTimes = new LinkedList<>(); + // 2. Calculate all possible refresh intervals in one round + for (long interval : metricsIntervals) { + for (long t = interval; t <= lcm; t += interval) { + if (!refreshTimes.contains(t)) { + refreshTimes.add(t); + } + } + } + // 3. Sort from smallest to largest + Collections.sort(refreshTimes); + // 4. Calculate the refresh interval list for Job's cycle + LinkedList<Long> intervals = new LinkedList<>(); + intervals.add(refreshTimes.get(0)); + for (int i = 1; i < refreshTimes.size(); i++) { + intervals.add(refreshTimes.get(i) - refreshTimes.get(i - 1)); + } + setIntervals(new ConcurrentLinkedDeque<>(intervals)); + } + + public synchronized long getInterval() { + if (this.intervals != null && !this.intervals.isEmpty()) { + Long interval = this.intervals.removeFirst(); + if (interval != null) { + this.intervals.addLast(interval); + return interval; + } + } + return getDefaultInterval(); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java new file mode 100644 index 0000000..3b010bc --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java @@ -0,0 +1,406 @@ +/* + * 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.job; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.job.protocol.DnsProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.DnsSdProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.FtpProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.EurekaSdProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.ConsulSdProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.ModbusProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.NacosSdProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.PlcProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.IcmpProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.ImapProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.IpmiProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.JmxProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.KafkaProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.MemcachedProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.MongodbProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.MqttProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.NebulaGraphProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.NginxProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.NgqlProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.NtpProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.Pop3Protocol; +import org.apache.hertzbeat.common.entity.job.protocol.PrometheusProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.PushProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.RedfishProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.RedisProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.RocketmqProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.S7Protocol; +import org.apache.hertzbeat.common.entity.job.protocol.ScriptProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.SmtpProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.SnmpProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.SshProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.TelnetProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.UdpProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.WebsocketProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.entity.job.protocol.ZookeeperSdProtocol; +/** + * Details of the monitoring metrics collected + * eg: cpu | memory | health + */ + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Slf4j +@SuppressWarnings("checkstyle:MemberName") +public class Metrics { + + /** + * public property-name eg: cpu | memory | health + */ + private String name; + /** + * metrics name's i18n value + * zh-CN: CPU信息 + * en-US: CPU Info + */ + private Map<String, String> i18n; + /** + * collect protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + */ + private String protocol; + /** + * Range (0-127) metrics scheduling priority, the smaller the value, the higher the priority + * The collection task of the next priority metrics will be scheduled only after the scheduled collection with the higher priority is completed. + * The default priority of the availability metrics is 0, and the range of other common metrics is 1-127, that is, + * the subsequent metrics tasks will only be scheduled after the availability is collected successfully. + */ + private Byte priority; + /** + * The latest collect time + */ + private long collectTime; + /** + * Customize interval of metrics + * If not set, the collection interval set in the front-end page will be used by default + */ + private long interval; + /** + * Is it visible true or false + * if false, web ui will not see this metrics. + */ + private boolean visible = true; + /** + * Public attribute - collection and monitoring final result attribute set eg: speed | times | size + */ + private List<Field> fields; + /** + * Public attribute - collection and monitoring pre-query attribute set eg: size1 | size2 | speedSize + */ + private List<String> aliasFields; + /** + * Public attribute - expression calculation, map the pre-query attribute (pre Fields) + * with the final attribute (fields), and calculate the final attribute (fields) value + * eg: size = size1 + size2, speed = speedSize + */ + private List<String> calculates; + /** + * filters + * eg: class == 9 && name != 'java' + */ + private List<String> filters; + /** + * unit conversion expr + * eg: + * - heap_used=B->MB + * - heap_total=B->MB + * - disk_free=B->GB + * - disk_total=B->GB + */ + private List<String> units; + /** + * Monitoring configuration information using the http protocol + */ + private HttpProtocol http; + /** + * Monitoring configuration information for ping using the icmp protocol + */ + private IcmpProtocol icmp; + /** + * Monitoring configuration information using the telnet protocol + */ + private TelnetProtocol telnet; + /** + * Monitoring configuration information using the public smtp protocol + */ + private SmtpProtocol smtp; + /** + * Monitoring configuration information using the public ntp protocol + */ + private NtpProtocol ntp; + /** + * Monitoring configuration information using the websocket protocol + */ + private WebsocketProtocol websocket; + /** + * Monitoring configuration information using the memcached protocol + */ + private MemcachedProtocol memcached; + /** + * Monitoring configuration information using the nebulaGraph protocol + */ + private NebulaGraphProtocol nebulaGraph; + /** + * Use udp implemented by socket for service port detection configuration information + */ + private UdpProtocol udp; + /** + * Database configuration information implemented using the public jdbc specification + */ + private JdbcProtocol jdbc; + /** + * Monitoring configuration information using the public ssh protocol + */ + private SshProtocol ssh; + /** + * Monitoring configuration information using the public redis protocol + */ + private RedisProtocol redis; + /** + * Monitoring configuration information using the public mongodb protocol + */ + private MongodbProtocol mongodb; + /** + * Get monitoring configuration information using public JMX protocol + */ + private JmxProtocol jmx; + /** + * Monitoring configuration information using the public snmp protocol + */ + private SnmpProtocol snmp; + /** + * Monitoring configuration information using the public ftp protocol + */ + private FtpProtocol ftp; + /** + * Monitoring configuration information using the public rocketmq protocol + */ + private RocketmqProtocol rocketmq; + /** + * Monitoring configuration information using push style + */ + private PushProtocol push; + /** + * Monitoring configuration information using the public prometheus protocol + */ + private PrometheusProtocol prometheus; + /** + * Monitoring configuration information using the public DNS protocol + */ + private DnsProtocol dns; + /** + * Monitoring configuration information using the public Nginx protocol + */ + private NginxProtocol nginx; + /** + * Monitoring configuration information using the public pop3 protocol + */ + private Pop3Protocol pop3; + /** + * Monitoring configuration information using the public registry protocol + */ + private RegistryProtocol registry; + /** + * Monitoring configuration information using the public redfish protocol + */ + private RedfishProtocol redfish; + /** + * Monitoring configuration information using the public ngql protocol + */ + private NgqlProtocol ngql; + /** + * Monitoring configuration information using the public imap protocol + */ + private ImapProtocol imap; + /** + * Monitoring configuration information using the public script protocol + */ + private ScriptProtocol script; + /** + * Monitoring configuration information using the public mqtt protocol + */ + private MqttProtocol mqtt; + /** + * Monitoring configuration information using the public ipmi protocol + */ + private IpmiProtocol ipmi; + /** + * Monitoring configuration information using the public kafka protocol + */ + private KafkaProtocol kclient; + /** + * http sd protocol + */ + private HttpProtocol http_sd; + /** + * dns sd protocol + */ + private DnsSdProtocol dns_sd; + /** + * eureka sd protocol + */ + private EurekaSdProtocol eureka_sd; + /** + * consul sd protocol + */ + private ConsulSdProtocol consul_sd; + /** + * nacos sd protocol + */ + private NacosSdProtocol nacos_sd; + /* + * zookeeper sd protocol + */ + private ZookeeperSdProtocol zookeeper_sd; + /** + * Monitoring configuration information using the public plc protocol + */ + private PlcProtocol plc; + /** + * Monitoring configuration information using the public modBus protocol + */ + private ModbusProtocol modbus; + /** + * Monitoring configuration information using the public s7 protocol + */ + private S7Protocol s7; + /** + * collector use - Temporarily store subTask metrics response data + */ + @JsonIgnore + private transient AtomicReference<CollectRep.MetricsData.Builder> subTaskDataRef; + + /** + * collector use - Temporarily store subTask running num + */ + @JsonIgnore + private transient AtomicInteger subTaskNum; + + /** + * collector use - Temporarily store subTask id + */ + @JsonIgnore + private transient Integer subTaskId; + + /** + * is has subTask + * + * @return true - has + */ + public boolean isHasSubTask() { + return subTaskNum != null; + } + + /** + * consume subTask + * + * @param metricsData response data + * @return is last task? + */ + public boolean consumeSubTaskResponse(CollectRep.MetricsData metricsData) { + synchronized (subTaskNum) { + int index = subTaskNum.decrementAndGet(); + if (subTaskDataRef.get() == null) { + subTaskDataRef.set(CollectRep.MetricsData.newBuilder(metricsData)); + } else { + if (metricsData.getValuesCount() >= 1) { + CollectRep.MetricsData.Builder dataBuilder = subTaskDataRef.get(); + for (CollectRep.ValueRow valueRow : metricsData.getValues()) { + if (valueRow.getColumnsCount() == dataBuilder.getFieldsCount()) { + dataBuilder.addValueRow(valueRow); + } else { + log.error("consume subTask data value not mapping filed"); + } + } + subTaskDataRef.set(dataBuilder); + } + } + metricsData.close(); + return index == 0; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Metrics metrics = (Metrics) o; + return name.equals(metrics.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + /** + * Metrics.Field + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + public static class Field { + /** + * Metric name + */ + private String field; + /** + * metric field name's i18n value + * CPU Version + */ + private Map<String, String> i18n; + /** + * Metric type 0-number: number 1-string: string + */ + private byte type = 1; + /** + * Whether this field is the label + */ + private boolean label = false; + /** + * Metric unit + */ + private String unit; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/RuntimeParamDefine.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/RuntimeParamDefine.java new file mode 100644 index 0000000..eae3e24 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/RuntimeParamDefine.java @@ -0,0 +1,76 @@ +/* + * 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.job; + +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Framework-agnostic parameter definition used by runtime templates and jobs. + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RuntimeParamDefine { + + private String app; + + private Map<String, String> name; + + private String field; + + private String type; + + private boolean required = false; + + private String defaultValue; + + private String placeholder; + + private String range; + + private Short limit; + + private List<Option> options; + + private String keyAlias; + + private String valueAlias; + + private boolean hide = false; + + private Map<String, List<Object>> depend; + + /** + * Runtime option definition. + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static final class Option { + + private String label; + + private String value; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/SshTunnel.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/SshTunnel.java new file mode 100644 index 0000000..7eb9bb2 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/SshTunnel.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.job.protocol.CommonRequestProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.Protocol; + +/** + * ssh tunnel + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SshTunnel implements CommonRequestProtocol, Protocol { + + /** + * enable ssh tunnel + */ + private String enable = "false"; + + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + + /** + * Peer host port + */ + private String port = "22"; + + /** + * TIME OUT PERIOD + */ + private String timeout = "6000"; + + /** + * UserName + */ + private String username; + + /** + * Password (optional) + */ + private String password; + + /** + * Private key (optional) + */ + private String privateKey; + + /** + * private key passphrase (optional) + */ + private String privateKeyPassphrase; + + /** + * share connection session + */ + private String shareConnection = "true"; + + @Override + public boolean isInvalid() { + + // todo add + return false; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/CommonRequestProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/CommonRequestProtocol.java new file mode 100644 index 0000000..74cb93a --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/CommonRequestProtocol.java @@ -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.job.protocol; + +/** + * Define common field method for each protocol in Metrics. + */ +public interface CommonRequestProtocol { + + void setHost(String host); + + void setPort(String port); +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ConsulSdProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ConsulSdProtocol.java new file mode 100644 index 0000000..a5b868b --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ConsulSdProtocol.java @@ -0,0 +1,42 @@ +/* + * 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.job.protocol; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * consul sd protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ConsulSdProtocol implements Protocol{ + + private String host; + + private String port; + + public boolean isInvalid() { + return StringUtils.isAnyBlank(host, port); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/DnsProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/DnsProtocol.java new file mode 100644 index 0000000..43c3102 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/DnsProtocol.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * Dns protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class DnsProtocol implements CommonRequestProtocol, Protocol { + private String dnsServerIP; + private String port; + private String address; + private String timeout; + private String tcp; + private String queryClass; + private String recordType; + + @Override + public boolean isInvalid() { + return StringUtils.isAnyBlank(dnsServerIP, port, address, timeout, tcp, queryClass, recordType); + } + + @Override + public void setHost(String host) { + this.address = host; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/DnsSdProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/DnsSdProtocol.java new file mode 100644 index 0000000..f2a5089 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/DnsSdProtocol.java @@ -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.common.entity.job.protocol; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +/** + * Dns protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class DnsSdProtocol implements Protocol { + + private String host; + + private String port; + + private String recordType; + + private String recordName; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isAnyBlank(recordType, recordName)) { + return true; + } + return !isValidRecordType(recordType); + } + + private boolean isValidRecordType(String type) { + return type.matches("^(?i)(A|AAAA|CNAME|MX|TXT|NS|SRV|PTR|SOA|CAA)$"); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/EurekaSdProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/EurekaSdProtocol.java new file mode 100644 index 0000000..e755330 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/EurekaSdProtocol.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import java.net.URI; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * eureka sd protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class EurekaSdProtocol implements Protocol{ + + private String url; + + @Override + public boolean isInvalid() { + if (StringUtils.isBlank(url)) { + return true; + } + try { + URI uri = URI.create(url.trim()); + return StringUtils.isBlank(uri.getHost()) + || (!"http".equalsIgnoreCase(uri.getScheme()) && !"https".equalsIgnoreCase(uri.getScheme())); + } catch (Exception e) { + return true; + } + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocol.java new file mode 100644 index 0000000..3767620 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocol.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * ftp protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class FtpProtocol implements CommonRequestProtocol, Protocol { + /** + * Peer host ip or domain name + */ + private String host; + + /** + * port number + */ + private String port; + + /** + * Redis Username (optional) + */ + private String username; + + /** + * Redis password(optional) + */ + private String password; + + /** + * file catalog + */ + private String direction; + + /** + * Timeout + */ + private String timeout; + + /** + * Whether ftp uses link encryption ssl/tls, i.e. ftp or sftp + * + */ + private String ssl = "false"; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port) || StringUtils.isBlank(direction) || StringUtils.isBlank(timeout)) { + return true; + } + if (!CommonUtil.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(ssl) + && !"true".equalsIgnoreCase(ssl) + && !"false".equalsIgnoreCase(ssl)) { + return true; + } + return "true".equalsIgnoreCase(ssl) && StringUtils.isAnyBlank(username, password); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/HttpProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/HttpProtocol.java new file mode 100644 index 0000000..73cb917 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/HttpProtocol.java @@ -0,0 +1,173 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * http protocol configuration + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class HttpProtocol implements CommonRequestProtocol, Protocol { + /** + * Peer host ip or domain name + */ + private String host; + /** + * port number + */ + private String port; + /** + * http/https The url link to which the request was made + */ + private String url; + /** + * timeout + */ + private String timeout; + /** + * Whether http uses link encryption ssl/tls, i.e. http or https + */ + private String ssl = "false"; + /** + * http request methods: get, post, put, delete, patch + */ + private String method; + /** + * http requests carry headers eg: Content-Type = application/json + */ + private Map<String, String> headers; + /** + * http requests carry query parameters eg: localhost:80/api? paramKey=value + */ + private Map<String, String> params; + /** + * The body carried by an http request + */ + private String payload; + /** + * authentication information + */ + private Authorization authorization; + /** + * How response data is parsed + * default - Own rules for parsing data + * json_path Custom jsonPath scripts <a href="https://www.jsonpath.cn/">...</a> + * xml_path Custom xmlPath scripts + * prometheus Prometheus Data Rules + */ + private String parseType; + /** + * Data parsing scripts exist when parsed as jsonPath or xmlPath + */ + private String parseScript; + /** + * Content keywords + */ + private String keyword; + + /** + * http success status code. default 200 + * successCode means what http response status code we consider it collect success. + */ + private List<String> successCodes; + + /** + * Whether to enable URL encoding for the path. Default is true. + * When true, the URL path will be encoded. When false, the URL path will not be encoded. + */ + private String enableUrlEncoding = "true"; + + /** + * authentication information + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Authorization { + /** + * certification type:Bearer Token, Basic Auth, Digest Auth + */ + private String type; + /** + * Bearer Token's token + */ + private String bearerTokenToken; + /** + * Basic Auth 's username + */ + private String basicAuthUsername; + /** + * Basic Auth 's password + */ + private String basicAuthPassword; + /** + * Digest Auth 's username + */ + private String digestAuthUsername; + /** + * Digest Auth 's password + */ + private String digestAuthPassword; + } + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(ssl) && !"true".equalsIgnoreCase(ssl) && !"false".equalsIgnoreCase(ssl)) { + return true; + } + if (StringUtils.isNotBlank(method) && !isValidHttpMethod(method)) { + return true; + } + if (StringUtils.isNotBlank(enableUrlEncoding) + && !"true".equalsIgnoreCase(enableUrlEncoding) + && !"false".equalsIgnoreCase(enableUrlEncoding)) { + return true; + } + return false; + } + + private boolean isValidHttpMethod(String method) { + return method.equalsIgnoreCase("get") + || method.equalsIgnoreCase("post") + || method.equalsIgnoreCase("put") + || method.equalsIgnoreCase("delete") + || method.equalsIgnoreCase("patch") + || method.equalsIgnoreCase("head") + || method.equalsIgnoreCase("options"); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/HttpSdProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/HttpSdProtocol.java new file mode 100644 index 0000000..e30a1f9 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/HttpSdProtocol.java @@ -0,0 +1,44 @@ +/* + * 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.job.protocol; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * Dns protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class HttpSdProtocol implements Protocol { + + private String url; + + @Override + public boolean isInvalid() { + if (StringUtils.isBlank(url)) { + return true; + } + return !url.matches("^(?i)https?://.+"); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/IcmpProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/IcmpProtocol.java new file mode 100644 index 0000000..d653396 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/IcmpProtocol.java @@ -0,0 +1,59 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * ICMP (PING) PROTOCOL CONFIGURATION + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class IcmpProtocol implements CommonRequestProtocol, Protocol { + + /** + * ip address or domain name of the peer host + */ + private String host; + + /** + * time out period + */ + private String timeout; + + @Override + public void setPort(String port) { + } + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host)) { + return true; + } + return StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ImapProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ImapProtocol.java new file mode 100644 index 0000000..f08103e --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ImapProtocol.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * imap protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ImapProtocol implements CommonRequestProtocol, Protocol { + /** + * Receiving server address + */ + private String host; + + /** + * Receiving server port + */ + private String port; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + /** + * Whether to enable SSL encryption [Email Transmission] + */ + private String ssl = "false"; + + /** + * IMAP Email address + */ + private String email; + + /** + * Authorization code + */ + private String authorize; + /** + * Mailbox folder name + */ + private String folderName; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isAnyBlank(timeout, email, authorize, folderName)) { + return true; + } + if (!CommonUtil.isNumeric(timeout)) { + return true; + } + return StringUtils.isNotBlank(ssl) + && !"true".equalsIgnoreCase(ssl) + && !"false".equalsIgnoreCase(ssl); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/IpmiProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/IpmiProtocol.java new file mode 100644 index 0000000..af61fa3 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/IpmiProtocol.java @@ -0,0 +1,95 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * IPMI2 Protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class IpmiProtocol implements CommonRequestProtocol, Protocol { + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + + /** + * Peer host port + */ + private String port; + + /** + * UserName + */ + private String username; + + /** + * Password + */ + private String password; + + /** + * command type: Chassis | Sensor | Raw + */ + private String type; + + /** + * (Optional) only used for the specific id search + */ + private String id; + + /** + * (Optional) only used when type is Raw + */ + private Field field; + + + static class Field { + + } + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port) || Integer.parseInt(port) <= 0) { + return true; + } + if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { + return true; + } + if (StringUtils.isBlank(type)) { + return true; + } + if (!"Chassis".equalsIgnoreCase(type) + && !"Sensor".equalsIgnoreCase(type) + && !"Raw".equalsIgnoreCase(type)) { + return true; + } + return "Raw".equalsIgnoreCase(type) && field == null; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/JdbcProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/JdbcProtocol.java new file mode 100644 index 0000000..47fe9ce --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/JdbcProtocol.java @@ -0,0 +1,105 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.entity.job.SshTunnel; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * Database configuration information implemented by the common jdbc specification + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class JdbcProtocol implements CommonRequestProtocol, Protocol { + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + /** + * Port number + */ + private String port; + /** + * Database user name (optional) + */ + private String username; + /** + * Database Password (optional) + */ + private String password; + /** + * Database + */ + private String database; + /** + * TIME OUT PERIOD + */ + private String timeout; + /** + * TYPE OF DATABASE mysql oracle ... + */ + private String platform; + /** + * SQL query mode: oneRow, multiRow, columns, runScript + */ + private String queryType; + /** + * sql + */ + private String sql; + /** + * DATABASE LINK URL eg: jdbc:mysql://localhost:3306/usthe + */ + private String url; + /** + * reuse connection session + */ + private String reuseConnection = "true"; + /** + * ssh tunnel + */ + private SshTunnel sshTunnel; + + @Override + public boolean isInvalid() { + if (StringUtils.isBlank(url)) { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(reuseConnection) + && !"true".equalsIgnoreCase(reuseConnection) + && !"false".equalsIgnoreCase(reuseConnection)) { + return true; + } + return false; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/JmxProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/JmxProtocol.java new file mode 100644 index 0000000..a5f5c88 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/JmxProtocol.java @@ -0,0 +1,96 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * Jmx protocol + **/ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class JmxProtocol implements CommonRequestProtocol, Protocol { + /** + * JMX host ip or domain name + */ + private String host; + + /** + * The port number + */ + private String port; + + /** + * enable ssl? + */ + private String ssl = "false"; + + /** + * Jmx username (optional) + */ + private String username; + + /** + * Jmx password (optional) + */ + private String password; + + /** + * jmx protocol custom collection metric address + */ + private String url; + + /** + * The name of the type where the outer layer of the jmx metric is located + */ + private String objectName; + + @Override + public boolean isInvalid() { + if (StringUtils.isBlank(objectName)) { + return true; + } + if (StringUtils.isNotBlank(ssl) + && !"true".equalsIgnoreCase(ssl) + && !"false".equalsIgnoreCase(ssl)) { + return true; + } + if (StringUtils.isNotBlank(username) && StringUtils.isBlank(password)) { + return true; + } + if (StringUtils.isBlank(username) && StringUtils.isNotBlank(password)) { + return true; + } + if (StringUtils.isNotBlank(url)) { + return !url.startsWith("service:jmx:rmi:") || url.contains("/stub/"); + } + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + return Integer.parseInt(port) <= 0; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/KafkaProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/KafkaProtocol.java new file mode 100644 index 0000000..eda0ae5 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/KafkaProtocol.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * Kafka protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class KafkaProtocol implements CommonRequestProtocol, Protocol { + + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + + /** + * Port number + */ + private String port; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + /** + * COMMAND + */ + private String command; + + /** + * Monitor internal topic + */ + private String monitorInternalTopic = "false"; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(monitorInternalTopic) + && !"true".equalsIgnoreCase(monitorInternalTopic) + && !"false".equalsIgnoreCase(monitorInternalTopic)) { + return true; + } + return false; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MemcachedProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MemcachedProtocol.java new file mode 100644 index 0000000..0b76de7 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MemcachedProtocol.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * memcached protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MemcachedProtocol implements CommonRequestProtocol, Protocol { + + /** + * MEMCACHED HOST IP ADDRESS OR DOMAIN NAME + */ + private String host; + + /** + * Memcached host port (default: 11211) + */ + private String port; + + @Override + public boolean isInvalid() { + return !validateIpDomain(host) || !validPort(port); + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ModbusProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ModbusProtocol.java new file mode 100644 index 0000000..4d7a08a --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ModbusProtocol.java @@ -0,0 +1,74 @@ +/* + * 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.job.protocol; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +import java.util.List; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + + +/** + * Modbus Protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ModbusProtocol implements Protocol { + + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + /** + * Port number + */ + private String port; + + private String driverName; + + private String addressSyntax; + + private String slaveId; + + private String timeout; + + private List<String> registerAddresses; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isAnyBlank(driverName, addressSyntax, slaveId)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + return registerAddresses == null || registerAddresses.isEmpty(); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MongodbProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MongodbProtocol.java new file mode 100644 index 0000000..68aa1af --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MongodbProtocol.java @@ -0,0 +1,96 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * Mongodb Protocol + * @version 1.0 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MongodbProtocol implements CommonRequestProtocol, Protocol { + + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + + /** + * Port number + */ + private String port; + + /** + * Mongodb model + */ + private String model; + + /** + * Mongodb User name (optional) + */ + private String username; + + /** + * Mongodb Password (optional) + */ + private String password; + + /** + * Mongodb database name (optional) + */ + private String database; + + /** + * Mongodb Authentication Database name (optional) + */ + private String authenticationDatabase; + + /** + * run command + */ + private String command; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + return false; + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MqttProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MqttProtocol.java new file mode 100644 index 0000000..b38665c --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/MqttProtocol.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * mqtt protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MqttProtocol implements CommonRequestProtocol, Protocol { + + /** + * mqtt client id + */ + private String clientId; + /** + * mqtt username + */ + private String username; + /** + * mqtt password + */ + private String password; + /** + * mqtt host + */ + private String host; + /** + * mqtt port + */ + private String port; + /** + * mqtt protocol version + * MQTT,MQTTS + */ + private String protocol; + /** + * mqtt connect timeout + * the maximum time to wait for a connection to be established + */ + private String timeout; + /** + * mqtt keepalive + * between ping requests to the broker to keep the connection alive + */ + private String keepalive; + /** + * mqtt topic name + */ + private String topic; + /** + * mqtt publish message + */ + private String testMessage; + /** + * mqtt tls version + * TLSv1.2, TLSv1.3 + */ + private String tlsVersion; + /** + * mqtt tls insecure skip verify server certificate + */ + private String insecureSkipVerify; + /** + * mqtt tls ca cert + */ + private String caCert; + /** + * mqtt tls enable mutual auth + */ + private String enableMutualAuth; + /** + * mqtt tls client cert + */ + private String clientCert; + /** + * mqtt tls client key + */ + private String clientKey; + + /** + * Determine whether authentication is required + * @return true if it has auth info + */ + public boolean hasAuth() { + return StringUtils.isNotBlank(this.username) && StringUtils.isNotBlank(this.password); + } + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(keepalive) && !CommonUtil.isNumeric(keepalive)) { + return true; + } + if (StringUtils.isNotBlank(protocol) + && !"MQTT".equalsIgnoreCase(protocol) + && !"MQTTS".equalsIgnoreCase(protocol)) { + return true; + } + if (StringUtils.isNotBlank(tlsVersion) + && !"TLSv1.2".equalsIgnoreCase(tlsVersion) + && !"TLSv1.3".equalsIgnoreCase(tlsVersion)) { + return true; + } + if (StringUtils.isNotBlank(insecureSkipVerify) + && !"true".equalsIgnoreCase(insecureSkipVerify) + && !"false".equalsIgnoreCase(insecureSkipVerify)) { + return true; + } + if (StringUtils.isNotBlank(enableMutualAuth) + && !"true".equalsIgnoreCase(enableMutualAuth) + && !"false".equalsIgnoreCase(enableMutualAuth)) { + return true; + } + if (StringUtils.isNotBlank(username) && StringUtils.isBlank(password)) { + return true; + } + if (StringUtils.isBlank(username) && StringUtils.isNotBlank(password)) { + return true; + } + if ("true".equalsIgnoreCase(enableMutualAuth)) { + if (StringUtils.isBlank(clientCert) || StringUtils.isBlank(clientKey)) { + return true; + } + } + return false; + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NacosSdProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NacosSdProtocol.java new file mode 100644 index 0000000..5e7c011 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NacosSdProtocol.java @@ -0,0 +1,74 @@ +/* + * 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.job.protocol; + +import org.apache.commons.lang3.StringUtils; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Nacos Service Discovery Protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class NacosSdProtocol implements Protocol { + /** + * Nacos server host + */ + private String host; + /** + * Nacos server port + */ + private String port; + /** + * Nacos namespace + */ + private String namespace; + /** + * Nacos username for authentication + */ + private String username; + /** + * Nacos password for authentication + */ + private String password; + + /** + * Nacos Service Name + */ + private String serviceName; + + /** + * Nacos Group Name + */ + + private String groupName; + + /** + * Check if the essential protocol parameters are invalid + * @return true if essential parameters are missing + */ + public boolean isInvalid() { + return StringUtils.isAnyBlank(host, port); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NebulaGraphProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NebulaGraphProtocol.java new file mode 100644 index 0000000..df6e8cf --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NebulaGraphProtocol.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * NebulaGraph Protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class NebulaGraphProtocol implements CommonRequestProtocol, Protocol { + /** + * NebulaGraph host ip or domain name + */ + private String host; + + /** + * NebulaGraph Graph service port default is 19669 + * NebulaGraph Storage service port is 19779 by default + */ + private String port; + + /** + * NebulaGraph Graph service monitoring API is /stats + * NebulaGraph Storage's service monitoring API is /rocksdb_stats + */ + private String url; + + /** + * NebulaGraph monitors time intervals + */ + private String timePeriod; + + /** + * TIMEOUT PERIOD + */ + private String timeout; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(timePeriod) && !CommonUtil.isNumeric(timePeriod)) { + return true; + } + return false; + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NginxProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NginxProtocol.java new file mode 100644 index 0000000..54a5d1b --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NginxProtocol.java @@ -0,0 +1,82 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * nginx protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class NginxProtocol implements CommonRequestProtocol, Protocol { + /** + * nginx Host ip address or domain name + */ + private String host; + + /** + * NGINX HOST PORT + */ + private String port; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + /** + * Monitor module page url + */ + private String url; + + /** + * Whether nginx uses link encryption ssl/tls, i.e. http or https + */ + private String ssl = "false"; + + /** + * Validates the relevant parameters + * @return is invalid true or false + */ + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(ssl) + && !"true".equalsIgnoreCase(ssl) + && !"false".equalsIgnoreCase(ssl)) { + return true; + } + return false; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NgqlProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NgqlProtocol.java new file mode 100644 index 0000000..1072366 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NgqlProtocol.java @@ -0,0 +1,98 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import java.util.List; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * NGQL protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class NgqlProtocol implements CommonRequestProtocol, Protocol { + private static final Set<String> VALID_PARSE_TYPES = Set.of("oneRow", "multiRow", "filterCount", "columns"); + + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + + /** + * Peer host port + */ + private String port; + + /** + * UserName + */ + private String username; + + /** + * Password + */ + private String password; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + /** + * SpaceName in nebula graph + */ + private String spaceName; + + /** + * ngql or open cypher + */ + private List<String> commands; + + /** + * how to parse data + */ + private String parseType; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isAnyBlank(username, password, timeout, parseType)) { + return true; + } + if (!VALID_PARSE_TYPES.contains(parseType) || !CommonUtil.isNumeric(timeout)) { + return true; + } + if (commands == null || commands.isEmpty()) { + return true; + } + return commands.stream().anyMatch(StringUtils::isBlank); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NtpProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NtpProtocol.java new file mode 100644 index 0000000..fd05ec0 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/NtpProtocol.java @@ -0,0 +1,63 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * Ntp Protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class NtpProtocol implements CommonRequestProtocol, Protocol { + /** + * NTP host ip address or domain name + */ + private String host; + + /** + * NTP host port + */ + private String port; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host)) { + return true; + } + if (StringUtils.isNotBlank(port) && !validPort(port)) { + return true; + } + return StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/PlcProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/PlcProtocol.java new file mode 100644 index 0000000..51024b1 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/PlcProtocol.java @@ -0,0 +1,74 @@ +/* + * 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.job.protocol; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +import java.util.List; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +/** + * Plc Protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PlcProtocol implements Protocol { + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + /** + * Port number + */ + private String port; + + private String driverName; + + private String addressSyntax; + + private String slaveId; + + private String timeout; + + private List<String> registerAddresses; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isAnyBlank(driverName, addressSyntax, slaveId)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + return registerAddresses == null || registerAddresses.isEmpty(); + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/Pop3Protocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/Pop3Protocol.java new file mode 100644 index 0000000..4adca6b --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/Pop3Protocol.java @@ -0,0 +1,68 @@ +/* + * 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.job.protocol; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * pop3 protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Pop3Protocol implements CommonRequestProtocol, Protocol { + /** + * Receiving server address + */ + private String host; + + /** + * Receiving server port + */ + private String port; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + /** + * Whether to enable SSL encryption [Email Transmission] + */ + private String ssl = "false"; + + /** + * pop Email address + */ + private String email; + + /** + * Authorization code + */ + private String authorize; + + @Override + public boolean isInvalid() { + return StringUtils.isAllBlank(host, port, timeout, ssl, email, authorize); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/PrometheusProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/PrometheusProtocol.java new file mode 100644 index 0000000..d460d5c --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/PrometheusProtocol.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * Prometheus Protocol configuration + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PrometheusProtocol implements CommonRequestProtocol, Protocol { + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + /** + * Peer host port + */ + private String port; + /** + * http/https metrics path + */ + private String path; + /** + * TIME OUT PERIOD + */ + private String timeout; + /** + * http Whether to use link-encrypted ssl/tls, that is, http or https + */ + private String ssl = "false"; + /** + * HTTP REQUEST METHOD: get, post, put, delete, patch + */ + private String method; + /** + * HTTP REQUESTS CARRY HEADERS eg: Content-Type = application/json + */ + private Map<String, String> headers; + /** + * HTTP REQUESTS CARRY QUERY PARAMETERS eg: localhost:80/api?paramKey=value + */ + private Map<String, String> params; + /** + * The request body carried by an http request + */ + private String payload; + /** + * Authentication information + */ + private Authorization authorization; + + /** + * Authentication information + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Authorization { + /** + * Authentication type:Bearer Token, Basic Auth, Digest Auth + */ + private String type; + /** + * Bearer Token's token + */ + private String bearerTokenToken; + /** + * Basic Auth 's username + */ + private String basicAuthUsername; + /** + * Basic Auth 's password + */ + private String basicAuthPassword; + /** + * Digest Auth 's username + */ + private String digestAuthUsername; + /** + * Digest Auth 's password + */ + private String digestAuthPassword; + } + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(ssl) && !"true".equalsIgnoreCase(ssl) && !"false".equalsIgnoreCase(ssl)) { + return true; + } + if (StringUtils.isNotBlank(method) && !isValidHttpMethod(method)) { + return true; + } + return false; + } + + private boolean isValidHttpMethod(String method) { + return method.equalsIgnoreCase("get") + || method.equalsIgnoreCase("post") + || method.equalsIgnoreCase("put") + || method.equalsIgnoreCase("delete") + || method.equalsIgnoreCase("patch") + || method.equalsIgnoreCase("head") + || method.equalsIgnoreCase("options"); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/Protocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/Protocol.java new file mode 100644 index 0000000..ad0becd --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/Protocol.java @@ -0,0 +1,31 @@ +/* + * 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.job.protocol; + +/** + * Base class for all protocols + */ +public interface Protocol { + + /** + * Check Protocol params valid. + * @return True or False. + */ + boolean isInvalid(); + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/PushProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/PushProtocol.java new file mode 100644 index 0000000..f032cc6 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/PushProtocol.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.isHasSchema; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.entity.dto.Field; + +/** + * push protocol definition + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PushProtocol implements CommonRequestProtocol, Protocol { + private String host; + private String port; + private String uri = "/api/push"; + private List<Field> fields; + + @Override + public boolean isInvalid() { + if ((!validateIpDomain(host) && !isHasSchema(host)) || !validPort(port)) { + return true; + } + if (Integer.parseInt(port) <= 0) { + return true; + } + if (StringUtils.isBlank(uri) || !uri.startsWith("/") || StringUtils.containsWhitespace(uri)) { + return true; + } + if (fields == null || fields.isEmpty()) { + return true; + } + for (Field field : fields) { + if (field == null + || StringUtils.isBlank(field.getName()) + || field.getType() == null + || (field.getType() != 0 && field.getType() != 1)) { + return true; + } + } + return false; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RedfishProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RedfishProtocol.java new file mode 100644 index 0000000..0a8c709 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RedfishProtocol.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.isHasSchema; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * Redfish Protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RedfishProtocol implements CommonRequestProtocol, Protocol { + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + + /** + * Peer host port + */ + private String port; + + /** + * UserName + */ + private String username; + + /** + * Password + */ + private String password; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + /** + * Redfish Resource Name and Corresponding Collection URI + */ + private String schema; + + private List<String> jsonPath; + + @Override + public boolean isInvalid() { + if ((!validateIpDomain(host) && !isHasSchema(host)) || !validPort(port)) { + return true; + } + if (Integer.parseInt(port) <= 0) { + return true; + } + if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { + return true; + } + if (!StringUtils.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(schema) + && (!schema.startsWith("/") || StringUtils.containsWhitespace(schema))) { + return true; + } + if (jsonPath == null || jsonPath.isEmpty()) { + return true; + } + for (String path : jsonPath) { + if (StringUtils.isBlank(path)) { + return true; + } + } + return false; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RedisProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RedisProtocol.java new file mode 100644 index 0000000..d96ef32 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RedisProtocol.java @@ -0,0 +1,93 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.entity.job.SshTunnel; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * Redis Protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RedisProtocol implements CommonRequestProtocol, Protocol { + + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + + /** + * Port number + */ + private String port; + + /** + * Redis User name (optional) + */ + private String username; + + /** + * Redis Password (optional) + */ + private String password; + + /** + * 1 - single 2 - sentinel 3 - cluster + */ + private String pattern; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + /** + * SSH TUNNEL + */ + private SshTunnel sshTunnel; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(pattern) && !isValidPattern(pattern)) { + return true; + } + return false; + } + + private boolean isValidPattern(String pattern) { + return "1".equals(pattern) || "2".equals(pattern) || "3".equals(pattern); + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RegistryProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RegistryProtocol.java new file mode 100644 index 0000000..aaa6316 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RegistryProtocol.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +/** + * Registry protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RegistryProtocol implements CommonRequestProtocol, Protocol { + private String host; + private String port; + private String discoveryClientTypeName; + + private String username; + private String password; + private String namespace; + private String serviceName; + private String groupName; + + @Override + public boolean isInvalid() { + + return validateIpDomain(host) && validPort(port) + && StringUtils.isAnyBlank(host, String.valueOf(port), discoveryClientTypeName); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RocketmqProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RocketmqProtocol.java new file mode 100644 index 0000000..b5f482b --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/RocketmqProtocol.java @@ -0,0 +1,86 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * rocketmq protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RocketmqProtocol implements CommonRequestProtocol, Protocol { + + /** + * rocketmq namesrv host + */ + private String namesrvHost; + + /** + * rocketmq namesrv port + */ + private String namesrvPort; + + /** + * accessKey + */ + private String accessKey; + + /** + * secretKey + */ + private String secretKey; + + /** + * jsonpath parsing script + */ + private String parseScript; + + @Override + public void setHost(String host) { + this.namesrvHost = host; + } + + @Override + public void setPort(String port) { + this.namesrvPort = port; + } + + @Override + public boolean isInvalid() { + if (!validateIpDomain(namesrvHost) || !validPort(namesrvPort)) { + return true; + } + if (StringUtils.isNotBlank(accessKey) && StringUtils.isBlank(secretKey)) { + return true; + } + if (StringUtils.isBlank(accessKey) && StringUtils.isNotBlank(secretKey)) { + return true; + } + return false; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/S7Protocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/S7Protocol.java new file mode 100644 index 0000000..d14b75b --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/S7Protocol.java @@ -0,0 +1,81 @@ +/* + * 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.job.protocol; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +import java.util.List; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + + +/** + * Modbus Protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class S7Protocol implements Protocol { + + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + /** + * Port number + */ + private String port; + + private String driverName; + + private String addressSyntax; + + private String rackId; + + private String slotId; + + private String controllerType; + + private String timeout; + + private List<String> registerAddresses; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isAnyBlank(driverName, addressSyntax, controllerType)) { + return true; + } + if (!CommonUtil.isNumeric(rackId) || !CommonUtil.isNumeric(slotId)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + return registerAddresses == null || registerAddresses.isEmpty(); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ScriptProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ScriptProtocol.java new file mode 100644 index 0000000..fb1877e --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ScriptProtocol.java @@ -0,0 +1,100 @@ +/* + * 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.job.protocol; + +import java.nio.charset.Charset; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * script protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ScriptProtocol implements CommonRequestProtocol, Protocol { + private static final Set<String> VALID_PARSE_TYPES = Set.of("oneRow", "multiRow", "netcat", "log"); + private static final Set<String> VALID_SCRIPT_TOOLS = Set.of("bash", "cmd", "powershell"); + + /** + * OS charset + */ + private String charset; + + /** + * Script working directory + */ + private String workDirectory; + + /** + * Script command + */ + private String scriptCommand; + + /** + * File name of script + */ + private String scriptPath; + + /** + * Response data parsing mode:oneRow, multiRow + */ + private String parseType; + + /** + * Script tool name, exp: bash, cmd, powershell + */ + private String scriptTool; + + @Override + public void setHost(String host) { + this.workDirectory = host; + } + + @Override + public void setPort(String port) { + this.scriptPath = port; + } + + @Override + public boolean isInvalid() { + if (StringUtils.isBlank(charset) || !isSupportedCharset(charset)) { + return true; + } + if (StringUtils.isBlank(parseType) || !VALID_PARSE_TYPES.contains(parseType)) { + return true; + } + if (StringUtils.isBlank(scriptTool) || !VALID_SCRIPT_TOOLS.contains(scriptTool)) { + return true; + } + return StringUtils.isAllBlank(scriptCommand, scriptPath); + } + + private boolean isSupportedCharset(String charsetName) { + try { + return Charset.isSupported(charsetName); + } catch (Exception e) { + return false; + } + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SmtpProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SmtpProtocol.java new file mode 100644 index 0000000..a8236b2 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SmtpProtocol.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * Smtp Protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SmtpProtocol implements CommonRequestProtocol, Protocol { + /** + * email Host ip address or domain name + */ + private String host; + + /** + * EMAIL HOST PORT + */ + private String port; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + /** + * helo command tester (email) + */ + private String email; + + /** + * Sent command + */ + private String cmd; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port) || StringUtils.isBlank(email)) { + return true; + } + return StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SnmpProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SnmpProtocol.java new file mode 100644 index 0000000..77428c9 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SnmpProtocol.java @@ -0,0 +1,151 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * snmp Protocol configuration + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SnmpProtocol implements CommonRequestProtocol, Protocol { + private static final String OPERATION_GET = "get"; + private static final String OPERATION_WALK = "walk"; + + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + /** + * Peer host port + */ + private String port; + /** + * timeout + */ + private String timeout; + /** + * snmp version v1 v2c v3 + * 0 = v1 + * 1 = v2c + * 3 = v3 + */ + private String version; + /** + * community name for v1 v2 + */ + private String community; + /** + * username (optional) + */ + private String username; + /** + * auth password (optional) + */ + private String authPassphrase; + /** + * password(optional) + */ + private String privPassphrase; + /** + * operation: get, walk + */ + private String operation = "get"; + /** + * oid map + */ + private Map<String, String> oids; + + /** + * contextName + */ + private String contextName; + + /** + * authPasswordEncryption + * v3 requires + */ + private String authPasswordEncryption; + + /** + * privPasswordEncryption + * v3 requires + */ + private String privPasswordEncryption; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port) || StringUtils.isBlank(version)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(operation) + && !OPERATION_GET.equalsIgnoreCase(operation) + && !OPERATION_WALK.equalsIgnoreCase(operation)) { + return true; + } + if (oids == null || oids.isEmpty()) { + return true; + } + for (Map.Entry<String, String> entry : oids.entrySet()) { + if (StringUtils.isAnyBlank(entry.getKey(), entry.getValue())) { + return true; + } + } + if (isVersion3()) { + return StringUtils.isAnyBlank(username, authPassphrase, privPassphrase); + } + if (!isVersion1Or2c()) { + return true; + } + return StringUtils.isBlank(community); + } + + private boolean isVersion1Or2c() { + return isVersion1() || isVersion2c(); + } + + private boolean isVersion1() { + return "0".equalsIgnoreCase(version) || "v1".equalsIgnoreCase(version); + } + + private boolean isVersion2c() { + return "1".equalsIgnoreCase(version) + || "2".equalsIgnoreCase(version) + || "2c".equalsIgnoreCase(version) + || "v2c".equalsIgnoreCase(version); + } + + private boolean isVersion3() { + return "3".equalsIgnoreCase(version) || "v3".equalsIgnoreCase(version); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SshProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SshProtocol.java new file mode 100644 index 0000000..1b79e45 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SshProtocol.java @@ -0,0 +1,144 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * ssh Protocol parameter configuration + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SshProtocol implements CommonRequestProtocol, Protocol { + + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + + /** + * Peer host port + */ + private String port; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + /** + * UserName + */ + private String username; + + /** + * Password (optional) + */ + private String password; + + /** + * Private key (optional) + */ + private String privateKey; + + /** + * private key passphrase (optional) + */ + private String privateKeyPassphrase; + + /** + * reuse connection session + */ + private String reuseConnection = "true"; + + /** + * SSH execution script + */ + private String script; + + /** + * Response data parsing mode:oneRow, multiRow + */ + private String parseType; + + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER PROXY HOST + */ + private String proxyHost; + + /** + * Peer proxy host port + */ + private String proxyPort; + + /** + * Proxy UserName + */ + private String proxyUsername; + + /** + * Proxy Password (optional) + */ + private String proxyPassword; + + /** + * flag of use proxy + */ + private String useProxy = "false"; + + /** + * Proxy private key (optional) + */ + private String proxyPrivateKey; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(reuseConnection) + && !"true".equalsIgnoreCase(reuseConnection) + && !"false".equalsIgnoreCase(reuseConnection)) { + return true; + } + if (StringUtils.isNotBlank(useProxy) + && !"true".equalsIgnoreCase(useProxy) + && !"false".equalsIgnoreCase(useProxy)) { + return true; + } + if ("true".equalsIgnoreCase(useProxy)) { + if (!validateIpDomain(proxyHost) || !validPort(proxyPort)) { + return true; + } + } + return false; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/TelnetProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/TelnetProtocol.java new file mode 100644 index 0000000..395db2e --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/TelnetProtocol.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * telnet Protocol configuration + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TelnetProtocol implements CommonRequestProtocol, Protocol { + + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + + /** + * Peer host port + */ + private String port; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + /** + * Sent command + */ + private String cmd; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + return StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout); + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/UdpProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/UdpProtocol.java new file mode 100644 index 0000000..73e7d3e --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/UdpProtocol.java @@ -0,0 +1,75 @@ +/* + * 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.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * Use udp implemented by socket for service port availability detection + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class UdpProtocol implements CommonRequestProtocol, Protocol { + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + + /** + * Port number + */ + private String port; + + /** + * TIME OUT PERIOD + */ + private String timeout; + + /** + * Custom protocol packet hexString Hexadecimal character string + */ + private String content; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (StringUtils.isNotBlank(timeout) && !CommonUtil.isNumeric(timeout)) { + return true; + } + if (StringUtils.isNotBlank(content) && !isValidHexString(content)) { + return true; + } + return false; + } + + private boolean isValidHexString(String hex) { + return hex.matches("^[0-9a-fA-F]+$"); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/WebsocketProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/WebsocketProtocol.java new file mode 100644 index 0000000..e643348 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/WebsocketProtocol.java @@ -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.common.entity.job.protocol; + +import static org.apache.hertzbeat.common.util.IpDomainUtil.validPort; +import static org.apache.hertzbeat.common.util.IpDomainUtil.validateIpDomain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * Websocket Protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class WebsocketProtocol implements CommonRequestProtocol, Protocol { + /** + * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST + */ + private String host; + + /** + * Port number + */ + private String port; + + /** + * The path to the websocket endpoint + */ + private String path; + + @Override + public boolean isInvalid() { + if (!validateIpDomain(host) || !validPort(port)) { + return true; + } + if (Integer.parseInt(port) <= 0) { + return true; + } + if (StringUtils.isEmpty(path)) { + return false; + } + return !path.startsWith("/") || StringUtils.containsWhitespace(path); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ZookeeperSdProtocol.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ZookeeperSdProtocol.java new file mode 100644 index 0000000..1799c87 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ZookeeperSdProtocol.java @@ -0,0 +1,48 @@ +/* + * 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.job.protocol; + +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * Zookeeper service discovery protocol + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ZookeeperSdProtocol implements Protocol{ + + private String url; + + private String pathPrefix; + + @Override + public boolean isInvalid() { + if (StringUtils.isAnyBlank(url, pathPrefix)) { + return true; + } + return StringUtils.containsWhitespace(url) + || StringUtils.containsWhitespace(pathPrefix) + || !pathPrefix.startsWith("/"); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/log/LogEntry.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/log/LogEntry.java new file mode 100644 index 0000000..dfbfc53 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/log/LogEntry.java @@ -0,0 +1,143 @@ +/* + * 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.log; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * OpenTelemetry Log Entry entity based on OpenTelemetry log data model specification. + * + * @see <a href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md">OpenTelemetry Log Data Model</a> + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LogEntry { + + /** + * Time when the event occurred. + * Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + */ + private Long timeUnixNano; + + /** + * Time when the event was observed. + * Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + */ + private Long observedTimeUnixNano; + + /** + * Numerical value of the severity. + * Smaller numerical values correspond to less severe events (such as debug events), + * larger numerical values correspond to more severe events (such as errors and critical events). + */ + private Integer severityNumber; + + /** + * The severity text (also known as log level). + * This is the original string representation of the severity as it is known at the source. + */ + private String severityText; + + /** + * A value containing the body of the log record. + * Can be for example a human-readable string message (including multi-line) + * or it can be a structured data composed of arrays and maps of other values. + */ + private Object body; + + /** + * Additional information about the specific event occurrence. + * Unlike the Resource field, these are NOT fixed for the lifetime of the process. + */ + private Map<String, Object> attributes; + + /** + * Dropped attributes count. + * Number of attributes that were discarded due to limits being exceeded. + */ + private Integer droppedAttributesCount; + + /** + * A unique identifier for a trace. + * All spans from the same trace share the same trace_id. + * The ID is a 16-byte array represented as a hex string. + */ + private String traceId; + + /** + * A unique identifier for a span within a trace. + * The ID is an 8-byte array represented as a hex string. + */ + private String spanId; + + /** + * Trace flag as defined in W3C Trace Context specification. + * At the time of writing the specification defines one flag - the SAMPLED flag. + */ + private Integer traceFlags; + + /** + * Resource information. + * Information about the entity producing the telemetry. + */ + private Map<String, Object> resource; + + /** + * Instrumentation Scope information. + * Information about the instrumentation scope that emitted the log. + */ + private InstrumentationScope instrumentationScope; + + /** + * Instrumentation Scope information for logs. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class InstrumentationScope { + + /** + * The name of the instrumentation scope. + * This should be the fully-qualified name of the instrumentation library. + */ + private String name; + + /** + * The version of the instrumentation scope. + */ + private String version; + + /** + * Additional attributes that describe the scope. + */ + private Map<String, Object> attributes; + + /** + * Number of attributes that were discarded due to limits being exceeded. + */ + private Integer droppedAttributesCount; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/message/ClusterMsg.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/message/ClusterMsg.java new file mode 100644 index 0000000..18181be --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/message/ClusterMsg.java @@ -0,0 +1,1449 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: cluster_msg.proto + +package org.apache.hertzbeat.common.entity.message; + +@SuppressWarnings("all") +public final class ClusterMsg { + private ClusterMsg() {} + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistryLite registry) { + } + + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistry registry) { + registerAllExtensions( + (com.google.protobuf.ExtensionRegistryLite) registry); + } + /** + * Protobuf enum {@code org.apache.hertzbeat.common.entity.message.MessageType} + */ + public enum MessageType + implements com.google.protobuf.ProtocolMessageEnum { + /** + * <pre> + * heartbeat message + * </pre> + * + * <code>HEARTBEAT = 0;</code> + */ + HEARTBEAT(0), + /** + * <pre> + * collector go online to master message + * </pre> + * + * <code>GO_ONLINE = 1;</code> + */ + GO_ONLINE(1), + /** + * <pre> + * collector go offline to master message + * </pre> + * + * <code>GO_OFFLINE = 2;</code> + */ + GO_OFFLINE(2), + /** + * <pre> + * collector go close to master + * </pre> + * + * <code>GO_CLOSE = 3;</code> + */ + GO_CLOSE(3), + /** + * <pre> + * issue cyclic collect task + * </pre> + * + * <code>ISSUE_CYCLIC_TASK = 4;</code> + */ + ISSUE_CYCLIC_TASK(4), + /** + * <pre> + * delete cyclic collect task + * </pre> + * + * <code>DELETE_CYCLIC_TASK = 5;</code> + */ + DELETE_CYCLIC_TASK(5), + /** + * <pre> + * issue one-time collect task + * </pre> + * + * <code>ISSUE_ONE_TIME_TASK = 6;</code> + */ + ISSUE_ONE_TIME_TASK(6), + /** + * <pre> + * response one-time collect data + * </pre> + * + * <code>RESPONSE_ONE_TIME_TASK_DATA = 7;</code> + */ + RESPONSE_ONE_TIME_TASK_DATA(7), + /** + * <pre> + * response cyclic collect data + * </pre> + * + * <code>RESPONSE_CYCLIC_TASK_DATA = 8;</code> + */ + RESPONSE_CYCLIC_TASK_DATA(8), + /** + * <pre> + * response cyclic service discovery data + * </pre> + * + * <code>RESPONSE_CYCLIC_TASK_SD_DATA = 9;</code> + */ + RESPONSE_CYCLIC_TASK_SD_DATA(9), + UNRECOGNIZED(-1), + ; + + /** + * <pre> + * heartbeat message + * </pre> + * + * <code>HEARTBEAT = 0;</code> + */ + public static final int HEARTBEAT_VALUE = 0; + /** + * <pre> + * collector go online to master message + * </pre> + * + * <code>GO_ONLINE = 1;</code> + */ + public static final int GO_ONLINE_VALUE = 1; + /** + * <pre> + * collector go offline to master message + * </pre> + * + * <code>GO_OFFLINE = 2;</code> + */ + public static final int GO_OFFLINE_VALUE = 2; + /** + * <pre> + * collector go close to master + * </pre> + * + * <code>GO_CLOSE = 3;</code> + */ + public static final int GO_CLOSE_VALUE = 3; + /** + * <pre> + * issue cyclic collect task + * </pre> + * + * <code>ISSUE_CYCLIC_TASK = 4;</code> + */ + public static final int ISSUE_CYCLIC_TASK_VALUE = 4; + /** + * <pre> + * delete cyclic collect task + * </pre> + * + * <code>DELETE_CYCLIC_TASK = 5;</code> + */ + public static final int DELETE_CYCLIC_TASK_VALUE = 5; + /** + * <pre> + * issue one-time collect task + * </pre> + * + * <code>ISSUE_ONE_TIME_TASK = 6;</code> + */ + public static final int ISSUE_ONE_TIME_TASK_VALUE = 6; + /** + * <pre> + * response one-time collect data + * </pre> + * + * <code>RESPONSE_ONE_TIME_TASK_DATA = 7;</code> + */ + public static final int RESPONSE_ONE_TIME_TASK_DATA_VALUE = 7; + /** + * <pre> + * response cyclic collect data + * </pre> + * + * <code>RESPONSE_CYCLIC_TASK_DATA = 8;</code> + */ + public static final int RESPONSE_CYCLIC_TASK_DATA_VALUE = 8; + /** + * <pre> + * response cyclic service discovery data + * </pre> + * + * <code>RESPONSE_CYCLIC_TASK_SD_DATA = 9;</code> + */ + public static final int RESPONSE_CYCLIC_TASK_SD_DATA_VALUE = 9; + + + public final int getNumber() { + if (this == UNRECOGNIZED) { + throw new IllegalArgumentException( + "Can't get the number of an unknown enum value."); + } + return value; + } + + /** + * @param value The numeric wire value of the corresponding enum entry. + * @return The enum associated with the given numeric wire value. + * @deprecated Use {@link #forNumber(int)} instead. + */ + @Deprecated + public static MessageType valueOf(int value) { + return forNumber(value); + } + + /** + * @param value The numeric wire value of the corresponding enum entry. + * @return The enum associated with the given numeric wire value. + */ + public static MessageType forNumber(int value) { + switch (value) { + case 0: return HEARTBEAT; + case 1: return GO_ONLINE; + case 2: return GO_OFFLINE; + case 3: return GO_CLOSE; + case 4: return ISSUE_CYCLIC_TASK; + case 5: return DELETE_CYCLIC_TASK; + case 6: return ISSUE_ONE_TIME_TASK; + case 7: return RESPONSE_ONE_TIME_TASK_DATA; + case 8: return RESPONSE_CYCLIC_TASK_DATA; + case 9: return RESPONSE_CYCLIC_TASK_SD_DATA; + default: return null; + } + } + + public static com.google.protobuf.Internal.EnumLiteMap<MessageType> + internalGetValueMap() { + return internalValueMap; + } + private static final com.google.protobuf.Internal.EnumLiteMap< + MessageType> internalValueMap = + new com.google.protobuf.Internal.EnumLiteMap<MessageType>() { + public MessageType findValueByNumber(int number) { + return MessageType.forNumber(number); + } + }; + + public final com.google.protobuf.Descriptors.EnumValueDescriptor + getValueDescriptor() { + if (this == UNRECOGNIZED) { + throw new IllegalStateException( + "Can't get the descriptor of an unrecognized enum value."); + } + return getDescriptor().getValues().get(ordinal()); + } + public final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptorForType() { + return getDescriptor(); + } + public static final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptor() { + return ClusterMsg.getDescriptor().getEnumTypes().get(0); + } + + private static final MessageType[] VALUES = values(); + + public static MessageType valueOf( + com.google.protobuf.Descriptors.EnumValueDescriptor desc) { + if (desc.getType() != getDescriptor()) { + throw new IllegalArgumentException( + "EnumValueDescriptor is not for this type."); + } + if (desc.getIndex() == -1) { + return UNRECOGNIZED; + } + return VALUES[desc.getIndex()]; + } + + private final int value; + + private MessageType(int value) { + this.value = value; + } + + // @@protoc_insertion_point(enum_scope:org.apache.hertzbeat.common.entity.message.MessageType) + } + + /** + * Protobuf enum {@code org.apache.hertzbeat.common.entity.message.Direction} + */ + public enum Direction + implements com.google.protobuf.ProtocolMessageEnum { + /** + * <pre> + * request message + * </pre> + * + * <code>REQUEST = 0;</code> + */ + REQUEST(0), + /** + * <pre> + * request response + * </pre> + * + * <code>RESPONSE = 1;</code> + */ + RESPONSE(1), + UNRECOGNIZED(-1), + ; + + /** + * <pre> + * request message + * </pre> + * + * <code>REQUEST = 0;</code> + */ + public static final int REQUEST_VALUE = 0; + /** + * <pre> + * request response + * </pre> + * + * <code>RESPONSE = 1;</code> + */ + public static final int RESPONSE_VALUE = 1; + + + public final int getNumber() { + if (this == UNRECOGNIZED) { + throw new IllegalArgumentException( + "Can't get the number of an unknown enum value."); + } + return value; + } + + /** + * @param value The numeric wire value of the corresponding enum entry. + * @return The enum associated with the given numeric wire value. + * @deprecated Use {@link #forNumber(int)} instead. + */ + @Deprecated + public static Direction valueOf(int value) { + return forNumber(value); + } + + /** + * @param value The numeric wire value of the corresponding enum entry. + * @return The enum associated with the given numeric wire value. + */ + public static Direction forNumber(int value) { + switch (value) { + case 0: return REQUEST; + case 1: return RESPONSE; + default: return null; + } + } + + public static com.google.protobuf.Internal.EnumLiteMap<Direction> + internalGetValueMap() { + return internalValueMap; + } + private static final com.google.protobuf.Internal.EnumLiteMap< + Direction> internalValueMap = + new com.google.protobuf.Internal.EnumLiteMap<Direction>() { + public Direction findValueByNumber(int number) { + return Direction.forNumber(number); + } + }; + + public final com.google.protobuf.Descriptors.EnumValueDescriptor + getValueDescriptor() { + if (this == UNRECOGNIZED) { + throw new IllegalStateException( + "Can't get the descriptor of an unrecognized enum value."); + } + return getDescriptor().getValues().get(ordinal()); + } + public final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptorForType() { + return getDescriptor(); + } + public static final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptor() { + return ClusterMsg.getDescriptor().getEnumTypes().get(1); + } + + private static final Direction[] VALUES = values(); + + public static Direction valueOf( + com.google.protobuf.Descriptors.EnumValueDescriptor desc) { + if (desc.getType() != getDescriptor()) { + throw new IllegalArgumentException( + "EnumValueDescriptor is not for this type."); + } + if (desc.getIndex() == -1) { + return UNRECOGNIZED; + } + return VALUES[desc.getIndex()]; + } + + private final int value; + + private Direction(int value) { + this.value = value; + } + + // @@protoc_insertion_point(enum_scope:org.apache.hertzbeat.common.entity.message.Direction) + } + + public interface MessageOrBuilder extends + // @@protoc_insertion_point(interface_extends:org.apache.hertzbeat.common.entity.message.Message) + com.google.protobuf.MessageOrBuilder { + + /** + * <pre> + * collector identity + * </pre> + * + * <code>string identity = 1;</code> + * @return The identity. + */ + String getIdentity(); + /** + * <pre> + * collector identity + * </pre> + * + * <code>string identity = 1;</code> + * @return The bytes for identity. + */ + com.google.protobuf.ByteString + getIdentityBytes(); + + /** + * <pre> + * message direction + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.Direction direction = 2;</code> + * @return The enum numeric value on the wire for direction. + */ + int getDirectionValue(); + /** + * <pre> + * message direction + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.Direction direction = 2;</code> + * @return The direction. + */ + Direction getDirection(); + + /** + * <pre> + * message type + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.MessageType type = 3;</code> + * @return The enum numeric value on the wire for type. + */ + int getTypeValue(); + /** + * <pre> + * message type + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.MessageType type = 3;</code> + * @return The type. + */ + MessageType getType(); + + /** + * <pre> + * message content + * </pre> + * + * <code>bytes msg = 4;</code> + * @return The msg. + */ + com.google.protobuf.ByteString getMsg(); + } + /** + * Protobuf type {@code org.apache.hertzbeat.common.entity.message.Message} + */ + public static final class Message extends + com.google.protobuf.GeneratedMessageV3 implements + // @@protoc_insertion_point(message_implements:org.apache.hertzbeat.common.entity.message.Message) + MessageOrBuilder { + private static final long serialVersionUID = 0L; + // Use Message.newBuilder() to construct. + private Message(com.google.protobuf.GeneratedMessageV3.Builder<?> builder) { + super(builder); + } + private Message() { + identity_ = ""; + direction_ = 0; + type_ = 0; + msg_ = com.google.protobuf.ByteString.EMPTY; + } + + @Override + @SuppressWarnings({"unused"}) + protected Object newInstance( + UnusedPrivateParameter unused) { + return new Message(); + } + + @Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private Message( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + this(); + if (extensionRegistry == null) { + throw new NullPointerException(); + } + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: { + String s = input.readStringRequireUtf8(); + + identity_ = s; + break; + } + case 16: { + int rawValue = input.readEnum(); + + direction_ = rawValue; + break; + } + case 24: { + int rawValue = input.readEnum(); + + type_ = rawValue; + break; + } + case 34: { + + msg_ = input.readBytes(); + break; + } + default: { + if (!parseUnknownField( + input, unknownFields, extensionRegistry, tag)) { + done = true; + } + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e).setUnfinishedMessage(this); + } finally { + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return ClusterMsg.internal_static_org_apache_hertzbeat_common_entity_message_Message_descriptor; + } + + @Override + protected FieldAccessorTable + internalGetFieldAccessorTable() { + return ClusterMsg.internal_static_org_apache_hertzbeat_common_entity_message_Message_fieldAccessorTable + .ensureFieldAccessorsInitialized( + Message.class, Builder.class); + } + + public static final int IDENTITY_FIELD_NUMBER = 1; + private volatile Object identity_; + /** + * <pre> + * collector identity + * </pre> + * + * <code>string identity = 1;</code> + * @return The identity. + */ + @Override + public String getIdentity() { + Object ref = identity_; + if (ref instanceof String) { + return (String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + String s = bs.toStringUtf8(); + identity_ = s; + return s; + } + } + /** + * <pre> + * collector identity + * </pre> + * + * <code>string identity = 1;</code> + * @return The bytes for identity. + */ + @Override + public com.google.protobuf.ByteString + getIdentityBytes() { + Object ref = identity_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (String) ref); + identity_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int DIRECTION_FIELD_NUMBER = 2; + private int direction_; + /** + * <pre> + * message direction + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.Direction direction = 2;</code> + * @return The enum numeric value on the wire for direction. + */ + @Override public int getDirectionValue() { + return direction_; + } + /** + * <pre> + * message direction + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.Direction direction = 2;</code> + * @return The direction. + */ + @Override public Direction getDirection() { + @SuppressWarnings("deprecation") + Direction result = Direction.valueOf(direction_); + return result == null ? Direction.UNRECOGNIZED : result; + } + + public static final int TYPE_FIELD_NUMBER = 3; + private int type_; + /** + * <pre> + * message type + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.MessageType type = 3;</code> + * @return The enum numeric value on the wire for type. + */ + @Override public int getTypeValue() { + return type_; + } + /** + * <pre> + * message type + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.MessageType type = 3;</code> + * @return The type. + */ + @Override public MessageType getType() { + @SuppressWarnings("deprecation") + MessageType result = MessageType.valueOf(type_); + return result == null ? MessageType.UNRECOGNIZED : result; + } + + public static final int MSG_FIELD_NUMBER = 4; + private com.google.protobuf.ByteString msg_; + /** + * <pre> + * message content + * </pre> + * + * <code>bytes msg = 4;</code> + * @return The msg. + */ + @Override + public com.google.protobuf.ByteString getMsg() { + return msg_; + } + + private byte memoizedIsInitialized = -1; + @Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(identity_)) { + com.google.protobuf.GeneratedMessageV3.writeString(output, 1, identity_); + } + if (direction_ != Direction.REQUEST.getNumber()) { + output.writeEnum(2, direction_); + } + if (type_ != MessageType.HEARTBEAT.getNumber()) { + output.writeEnum(3, type_); + } + if (!msg_.isEmpty()) { + output.writeBytes(4, msg_); + } + unknownFields.writeTo(output); + } + + @Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(identity_)) { + size += com.google.protobuf.GeneratedMessageV3.computeStringSize(1, identity_); + } + if (direction_ != Direction.REQUEST.getNumber()) { + size += com.google.protobuf.CodedOutputStream + .computeEnumSize(2, direction_); + } + if (type_ != MessageType.HEARTBEAT.getNumber()) { + size += com.google.protobuf.CodedOutputStream + .computeEnumSize(3, type_); + } + if (!msg_.isEmpty()) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(4, msg_); + } + size += unknownFields.getSerializedSize(); + memoizedSize = size; + return size; + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Message)) { + return super.equals(obj); + } + Message other = (Message) obj; + + if (!getIdentity() + .equals(other.getIdentity())) return false; + if (direction_ != other.direction_) return false; + if (type_ != other.type_) return false; + if (!getMsg() + .equals(other.getMsg())) return false; + if (!unknownFields.equals(other.unknownFields)) return false; + return true; + } + + @Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + IDENTITY_FIELD_NUMBER; + hash = (53 * hash) + getIdentity().hashCode(); + hash = (37 * hash) + DIRECTION_FIELD_NUMBER; + hash = (53 * hash) + direction_; + hash = (37 * hash) + TYPE_FIELD_NUMBER; + hash = (53 * hash) + type_; + hash = (37 * hash) + MSG_FIELD_NUMBER; + hash = (53 * hash) + getMsg().hashCode(); + hash = (29 * hash) + unknownFields.hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static Message parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static Message parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static Message parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static Message parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static Message parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static Message parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static Message parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static Message parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + public static Message parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input); + } + public static Message parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static Message parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static Message parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(Message prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @Override + protected Builder newBuilderForType( + BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code org.apache.hertzbeat.common.entity.message.Message} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageV3.Builder<Builder> implements + // @@protoc_insertion_point(builder_implements:org.apache.hertzbeat.common.entity.message.Message) + MessageOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return ClusterMsg.internal_static_org_apache_hertzbeat_common_entity_message_Message_descriptor; + } + + @Override + protected FieldAccessorTable + internalGetFieldAccessorTable() { + return ClusterMsg.internal_static_org_apache_hertzbeat_common_entity_message_Message_fieldAccessorTable + .ensureFieldAccessorsInitialized( + Message.class, Builder.class); + } + + // Construct using org.apache.hertzbeat.common.entity.message.ClusterMsg.Message.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessageV3 + .alwaysUseFieldBuilders) { + } + } + @Override + public Builder clear() { + super.clear(); + identity_ = ""; + + direction_ = 0; + + type_ = 0; + + msg_ = com.google.protobuf.ByteString.EMPTY; + + return this; + } + + @Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return ClusterMsg.internal_static_org_apache_hertzbeat_common_entity_message_Message_descriptor; + } + + @Override + public Message getDefaultInstanceForType() { + return Message.getDefaultInstance(); + } + + @Override + public Message build() { + Message result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @Override + public Message buildPartial() { + Message result = new Message(this); + result.identity_ = identity_; + result.direction_ = direction_; + result.type_ = type_; + result.msg_ = msg_; + onBuilt(); + return result; + } + + @Override + public Builder clone() { + return super.clone(); + } + @Override + public Builder setField( + com.google.protobuf.Descriptors.FieldDescriptor field, + Object value) { + return super.setField(field, value); + } + @Override + public Builder clearField( + com.google.protobuf.Descriptors.FieldDescriptor field) { + return super.clearField(field); + } + @Override + public Builder clearOneof( + com.google.protobuf.Descriptors.OneofDescriptor oneof) { + return super.clearOneof(oneof); + } + @Override + public Builder setRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + int index, Object value) { + return super.setRepeatedField(field, index, value); + } + @Override + public Builder addRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + Object value) { + return super.addRepeatedField(field, value); + } + @Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof Message) { + return mergeFrom((Message)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(Message other) { + if (other == Message.getDefaultInstance()) return this; + if (!other.getIdentity().isEmpty()) { + identity_ = other.identity_; + onChanged(); + } + if (other.direction_ != 0) { + setDirectionValue(other.getDirectionValue()); + } + if (other.type_ != 0) { + setTypeValue(other.getTypeValue()); + } + if (other.getMsg() != com.google.protobuf.ByteString.EMPTY) { + setMsg(other.getMsg()); + } + this.mergeUnknownFields(other.unknownFields); + onChanged(); + return this; + } + + @Override + public final boolean isInitialized() { + return true; + } + + @Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Message parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (Message) e.getUnfinishedMessage(); + throw e.unwrapIOException(); + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + + private Object identity_ = ""; + /** + * <pre> + * collector identity + * </pre> + * + * <code>string identity = 1;</code> + * @return The identity. + */ + public String getIdentity() { + Object ref = identity_; + if (!(ref instanceof String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + String s = bs.toStringUtf8(); + identity_ = s; + return s; + } else { + return (String) ref; + } + } + /** + * <pre> + * collector identity + * </pre> + * + * <code>string identity = 1;</code> + * @return The bytes for identity. + */ + public com.google.protobuf.ByteString + getIdentityBytes() { + Object ref = identity_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (String) ref); + identity_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * <pre> + * collector identity + * </pre> + * + * <code>string identity = 1;</code> + * @param value The identity to set. + * @return This builder for chaining. + */ + public Builder setIdentity( + String value) { + if (value == null) { + throw new NullPointerException(); + } + + identity_ = value; + onChanged(); + return this; + } + /** + * <pre> + * collector identity + * </pre> + * + * <code>string identity = 1;</code> + * @return This builder for chaining. + */ + public Builder clearIdentity() { + + identity_ = getDefaultInstance().getIdentity(); + onChanged(); + return this; + } + /** + * <pre> + * collector identity + * </pre> + * + * <code>string identity = 1;</code> + * @param value The bytes for identity to set. + * @return This builder for chaining. + */ + public Builder setIdentityBytes( + com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + checkByteStringIsUtf8(value); + + identity_ = value; + onChanged(); + return this; + } + + private int direction_ = 0; + /** + * <pre> + * message direction + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.Direction direction = 2;</code> + * @return The enum numeric value on the wire for direction. + */ + @Override public int getDirectionValue() { + return direction_; + } + /** + * <pre> + * message direction + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.Direction direction = 2;</code> + * @param value The enum numeric value on the wire for direction to set. + * @return This builder for chaining. + */ + public Builder setDirectionValue(int value) { + + direction_ = value; + onChanged(); + return this; + } + /** + * <pre> + * message direction + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.Direction direction = 2;</code> + * @return The direction. + */ + @Override + public Direction getDirection() { + @SuppressWarnings("deprecation") + Direction result = Direction.valueOf(direction_); + return result == null ? Direction.UNRECOGNIZED : result; + } + /** + * <pre> + * message direction + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.Direction direction = 2;</code> + * @param value The direction to set. + * @return This builder for chaining. + */ + public Builder setDirection(Direction value) { + if (value == null) { + throw new NullPointerException(); + } + + direction_ = value.getNumber(); + onChanged(); + return this; + } + /** + * <pre> + * message direction + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.Direction direction = 2;</code> + * @return This builder for chaining. + */ + public Builder clearDirection() { + + direction_ = 0; + onChanged(); + return this; + } + + private int type_ = 0; + /** + * <pre> + * message type + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.MessageType type = 3;</code> + * @return The enum numeric value on the wire for type. + */ + @Override public int getTypeValue() { + return type_; + } + /** + * <pre> + * message type + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.MessageType type = 3;</code> + * @param value The enum numeric value on the wire for type to set. + * @return This builder for chaining. + */ + public Builder setTypeValue(int value) { + + type_ = value; + onChanged(); + return this; + } + /** + * <pre> + * message type + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.MessageType type = 3;</code> + * @return The type. + */ + @Override + public MessageType getType() { + @SuppressWarnings("deprecation") + MessageType result = MessageType.valueOf(type_); + return result == null ? MessageType.UNRECOGNIZED : result; + } + /** + * <pre> + * message type + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.MessageType type = 3;</code> + * @param value The type to set. + * @return This builder for chaining. + */ + public Builder setType(MessageType value) { + if (value == null) { + throw new NullPointerException(); + } + + type_ = value.getNumber(); + onChanged(); + return this; + } + /** + * <pre> + * message type + * </pre> + * + * <code>.org.apache.hertzbeat.common.entity.message.MessageType type = 3;</code> + * @return This builder for chaining. + */ + public Builder clearType() { + + type_ = 0; + onChanged(); + return this; + } + + private com.google.protobuf.ByteString msg_ = com.google.protobuf.ByteString.EMPTY; + /** + * <pre> + * message content + * </pre> + * + * <code>bytes msg = 4;</code> + * @return The msg. + */ + @Override + public com.google.protobuf.ByteString getMsg() { + return msg_; + } + /** + * <pre> + * message content + * </pre> + * + * <code>bytes msg = 4;</code> + * @param value The msg to set. + * @return This builder for chaining. + */ + public Builder setMsg(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + + msg_ = value; + onChanged(); + return this; + } + /** + * <pre> + * message content + * </pre> + * + * <code>bytes msg = 4;</code> + * @return This builder for chaining. + */ + public Builder clearMsg() { + + msg_ = getDefaultInstance().getMsg(); + onChanged(); + return this; + } + @Override + public final Builder setUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.setUnknownFields(unknownFields); + } + + @Override + public final Builder mergeUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.mergeUnknownFields(unknownFields); + } + + + // @@protoc_insertion_point(builder_scope:org.apache.hertzbeat.common.entity.message.Message) + } + + // @@protoc_insertion_point(class_scope:org.apache.hertzbeat.common.entity.message.Message) + private static final Message DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new Message(); + } + + public static Message getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser<Message> + PARSER = new com.google.protobuf.AbstractParser<Message>() { + @Override + public Message parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new Message(input, extensionRegistry); + } + }; + + public static com.google.protobuf.Parser<Message> parser() { + return PARSER; + } + + @Override + public com.google.protobuf.Parser<Message> getParserForType() { + return PARSER; + } + + @Override + public Message getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_org_apache_hertzbeat_common_entity_message_Message_descriptor; + private static final + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internal_static_org_apache_hertzbeat_common_entity_message_Message_fieldAccessorTable; + + public static com.google.protobuf.Descriptors.FileDescriptor + getDescriptor() { + return descriptor; + } + private static com.google.protobuf.Descriptors.FileDescriptor + descriptor; + static { + String[] descriptorData = { + "\n\021cluster_msg.proto\022*org.apache.hertzbea" + + "t.common.entity.message\"\271\001\n\007Message\022\020\n\010i" + + "dentity\030\001 \001(\t\022H\n\tdirection\030\002 \001(\01625.org.a" + + "pache.hertzbeat.common.entity.message.Di" + + "rection\022E\n\004type\030\003 \001(\01627.org.apache.hertz" + + "beat.common.entity.message.MessageType\022\013" + + "\n\003msg\030\004 \001(\014*\363\001\n\013MessageType\022\r\n\tHEARTBEAT" + + "\020\000\022\r\n\tGO_ONLINE\020\001\022\016\n\nGO_OFFLINE\020\002\022\014\n\010GO_" + + "CLOSE\020\003\022\025\n\021ISSUE_CYCLIC_TASK\020\004\022\026\n\022DELETE" + + "_CYCLIC_TASK\020\005\022\027\n\023ISSUE_ONE_TIME_TASK\020\006\022" + + "\037\n\033RESPONSE_ONE_TIME_TASK_DATA\020\007\022\035\n\031RESP" + + "ONSE_CYCLIC_TASK_DATA\020\010\022 \n\034RESPONSE_CYCL" + + "IC_TASK_SD_DATA\020\t*&\n\tDirection\022\013\n\007REQUES" + + "T\020\000\022\014\n\010RESPONSE\020\001b\006proto3" + }; + descriptor = com.google.protobuf.Descriptors.FileDescriptor + .internalBuildGeneratedFileFrom(descriptorData, + new com.google.protobuf.Descriptors.FileDescriptor[] { + }); + internal_static_org_apache_hertzbeat_common_entity_message_Message_descriptor = + getDescriptor().getMessageTypes().get(0); + internal_static_org_apache_hertzbeat_common_entity_message_Message_fieldAccessorTable = new + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( + internal_static_org_apache_hertzbeat_common_entity_message_Message_descriptor, + new String[] { "Identity", "Direction", "Type", "Msg", }); + } + + // @@protoc_insertion_point(outer_class_scope) +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/message/CollectRep.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/message/CollectRep.java new file mode 100644 index 0000000..03d2821 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/message/CollectRep.java @@ -0,0 +1,754 @@ +/* + * 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.message; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.VarCharVector; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.ipc.ArrowStreamWriter; +import org.apache.arrow.vector.table.ArrowTable; +import org.apache.arrow.vector.table.Row; +import org.apache.arrow.vector.types.pojo.ArrowType; +import org.apache.arrow.vector.types.pojo.FieldType; +import org.apache.arrow.vector.types.pojo.Schema; +import org.apache.hertzbeat.common.constants.MetricDataConstants; +import org.apache.hertzbeat.common.entity.arrow.RowWrapper; +import org.apache.hertzbeat.common.util.JsonUtil; + +@SuppressWarnings("all") +@Slf4j +public final class CollectRep { + private CollectRep() {} + + /** + * Collect response status code enum + */ + public enum Code { + /** + * collect success + */ + SUCCESS(0), + + /** + * collector not available + */ + UN_AVAILABLE(1), + + /** + * peer network un reachable(icmp) + */ + UN_REACHABLE(2), + + /** + * peer network server un connectable(tcp,udp...) + */ + UN_CONNECTABLE(3), + + /** + * collect metrics data failed(http,ssh,snmp...) + */ + FAIL(4), + + /** + * collect metrics data timeout + */ + TIMEOUT(5), + + /** + * unrecognized collect response code + */ + UNRECOGNIZED(-1); + + private final int value; + + Code(int value) { + this.value = value; + } + + public int getNumber() { + if (this == UNRECOGNIZED) { + throw new IllegalArgumentException("Can't get the number of an unknown enum value."); + } + return value; + } + + public static Code forNumber(int value) { + switch (value) { + case 0: return SUCCESS; + case 1: return UN_AVAILABLE; + case 2: return UN_REACHABLE; + case 3: return UN_CONNECTABLE; + case 4: return FAIL; + case 5: return TIMEOUT; + default: return null; + } + } + } + + /** + * Metrics data collect response entity + */ + public static class MetricsData implements AutoCloseable { + + /** + * arrow collect metrics data table + */ + private ArrowTable table; + + public MetricsData(ArrowTable table) { + this.table = table; + } + + public MetricsData(VectorSchemaRoot vectorSchemaRoot) { + this.table = new ArrowTable(vectorSchemaRoot); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static Builder newBuilder(MetricsData metricsData) { + Builder builder = new Builder(); + builder.addMetadataAll(metricsData.getMetadata()); + metricsData.getFields().forEach(builder::addField); + metricsData.getValues().forEach(builder::addValueRow); + return builder; + } + + public long rowCount() { + return table != null ? table.getRowCount() : 0; + } + + public ArrowTable getTable() { + return table; + } + + /** + * notice is to vectorschemaRoot for arrow, the table will empty + * @return + */ + public VectorSchemaRoot toVectorSchemaRootAndRelease() { + return table != null ? table.toVectorSchemaRoot() : null; + } + + /** + * to byte array and release the table + * @return + */ + public byte[] toByteArrayAndRelease() { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + VectorSchemaRoot root = table.toVectorSchemaRoot(); + ArrowStreamWriter writer = new ArrowStreamWriter(root, + null, Channels.newChannel(out))) { + writer.start(); + writer.writeBatch(); + writer.end(); + return out.toByteArray(); + } catch (IOException e) { + log.error(e.getMessage(), e); + return null; + } + } + + public long getId() { + Map<String, String> metadata = getMetadata(); + return Long.parseLong(metadata.getOrDefault(MetricDataConstants.ID, "0")); + } + + public long getTenantId() { + Map<String, String> metadata = getMetadata(); + return Long.parseLong(metadata.getOrDefault(MetricDataConstants.TENANT_ID, "0")); + } + + public String getApp() { + Map<String, String> metadata = getMetadata(); + return metadata.getOrDefault(MetricDataConstants.APP, ""); + } + + public String getMetrics() { + Map<String, String> metadata = getMetadata(); + return metadata.getOrDefault(MetricDataConstants.METRICS, ""); + } + + public int getPriority() { + Map<String, String> metadata = getMetadata(); + return Integer.parseInt(metadata.getOrDefault(MetricDataConstants.PRIORITY, "0")); + } + + public long getTime() { + Map<String, String> metadata = getMetadata(); + return Long.parseLong(metadata.getOrDefault(MetricDataConstants.TIME, "0")); + } + + public Code getCode() { + Map<String, String> metadata = getMetadata(); + return Code.forNumber(Integer.parseInt(metadata.getOrDefault(MetricDataConstants.CODE, "0"))); + } + + public String getMsg() { + Map<String, String> metadata = getMetadata(); + return metadata.getOrDefault(MetricDataConstants.MSG, ""); + } + + public String getInstanceName() { + Map<String, String> metadata = getMetadata(); + return metadata.getOrDefault(MetricDataConstants.INSTANCE_NAME, null); + } + + public String getInstance() { + Map<String, String> metadata = getMetadata(); + return metadata.getOrDefault(MetricDataConstants.INSTANCE, null); + } + + public Map<String, String> getLabels() { + Map<String, String> metadata = getMetadata(); + String labelStr = metadata.getOrDefault(MetricDataConstants.LABELS, ""); + if (labelStr == null || "".equals(labelStr)) { + return null; + } + return JsonUtil.fromJson(labelStr, Map.class); + } + + public Map<String, String> getAnnotations() { + Map<String, String> metadata = getMetadata(); + String annotationStr = metadata.getOrDefault(MetricDataConstants.ANNOTATIONS, ""); + if (annotationStr == null || "".equals(annotationStr)) { + return null; + } + return JsonUtil.fromJson(annotationStr, Map.class); + } + + private Map<String, String> getMetadata() { + return table != null ? table.getSchema().getCustomMetadata() : new HashMap<>(); + } + + // Get fields and values from table + public List<Field> getFields() { + if (table == null) { + return new ArrayList<>(); + } + return table.getSchema().getFields().stream() + .map(field -> { + Map<String, String> metadata = field.getMetadata(); + return Field.newBuilder() + .setName(field.getName()) + .setType(Integer.parseInt(metadata.getOrDefault(MetricDataConstants.TYPE, "0"))) + .setUnit(metadata.getOrDefault(MetricDataConstants.UNIT, "")) + .setLabel(Boolean.parseBoolean(metadata.getOrDefault(MetricDataConstants.LABEL, "false"))) + .build(); + }) + .collect(Collectors.toList()); + } + + public List<ValueRow> getValues() { + if (table == null) { + return new ArrayList<>(); + } + + List<String> fieldNames = table.getSchema().getFields().stream() + .map(org.apache.arrow.vector.types.pojo.Field::getName) + .collect(Collectors.toList()); + + List<ValueRow> values = new ArrayList<>(); + Iterator<Row> iterator = table.iterator(); + while (iterator.hasNext()) { + Row row = iterator.next(); + ValueRow valueRow = ValueRow.newBuilder() + .setColumns(fieldNames.stream() + .map(fieldName -> new String(((VarCharVector) + table.getVector(fieldName)).get(row.getRowNumber()))) + .collect(Collectors.toList())) + .build(); + values.add(valueRow); + } + return values; + } + + public RowWrapper readRow() { + final Iterator<Row> rowIterator = table.iterator(); + List<org.apache.arrow.vector.types.pojo.Field> fields = table.getSchema().getFields(); + return new RowWrapper(null, rowIterator, fields, -1); + } + + public int getValuesCount() { + return table != null ? (int)table.getRowCount() : 0; + } + + public int getFieldsCount() { + return table != null ? table.getSchema().getFields().size() : 0; + } + + @Override + public void close() { + if (table != null) { + table.close(); + } + } + + // Builder remains mostly the same, but build() method changes + public static class Builder { + + private Map<String, String> metadata = new HashMap<>(); + private List<Field> fields = new ArrayList<>(); + private List<ValueRow> values = new ArrayList<>(); + + public Builder setId(long id) { + metadata.put(MetricDataConstants.ID, String.valueOf(id)); + return this; + } + + public Builder setTenantId(long tenantId) { + metadata.put(MetricDataConstants.TENANT_ID, String.valueOf(tenantId)); + return this; + } + + public Builder setApp(String app) { + metadata.put(MetricDataConstants.APP, app != null ? app : ""); + return this; + } + + public Builder setMetrics(String metrics) { + metadata.put(MetricDataConstants.METRICS, metrics != null ? metrics : ""); + return this; + } + + public Builder setLabels(Map<String, String> labels) { + if (labels == null || labels.isEmpty()) { + return this; + } + String labelStr = JsonUtil.toJson(labels); + metadata.put(MetricDataConstants.LABELS, labelStr); + return this; + } + + public Builder setAnnotations(Map<String, String> annotations) { + if (annotations == null || annotations.isEmpty()) { + return this; + } + String annotationStr = JsonUtil.toJson(annotations); + metadata.put(MetricDataConstants.ANNOTATIONS, annotationStr); + return this; + } + + public Builder setPriority(int priority) { + metadata.put(MetricDataConstants.PRIORITY, String.valueOf(priority)); + return this; + } + + public Builder setTime(long time) { + metadata.put(MetricDataConstants.TIME, String.valueOf(time)); + return this; + } + + public Builder setCode(Code code) { + metadata.put(MetricDataConstants.CODE, String.valueOf(code != null ? code.value : 0)); + return this; + } + + public Builder setMsg(String msg) { + metadata.put(MetricDataConstants.MSG, msg != null ? msg : ""); + return this; + } + + public Builder addMetadata(String key, String value) { + if (key == null || "".equals(key)) { + return this; + } + metadata.put(key, value); + return this; + } + + public Builder addMetadataAll(Map<String, String> meta) { + if (meta == null || meta.isEmpty()) { + return this; + } + this.metadata.putAll(meta); + return this; + } + + public Builder addField(Field field) { + this.fields.add(field); + return this; + } + + public Builder addValueRow(ValueRow valueRow) { + this.values.add(valueRow); + return this; + } + + public MetricsData build() { + BufferAllocator allocator = new RootAllocator(); + try { + // Create Arrow fields with metadata + List<org.apache.arrow.vector.types.pojo.Field> arrowFields = fields.stream() + .map(field -> { + Map<String, String> fieldMetadata = new HashMap<>(); + fieldMetadata.put(MetricDataConstants.TYPE, String.valueOf(field.getType())); + fieldMetadata.put(MetricDataConstants.UNIT, field.getUnit()); + fieldMetadata.put(MetricDataConstants.LABEL, String.valueOf(field.getLabel())); + + return new org.apache.arrow.vector.types.pojo.Field( + field.getName(), + new FieldType(true, new ArrowType.Utf8(), null, fieldMetadata), + null); + }) + .collect(Collectors.toList()); + + // Create Schema with metadata + Schema schema = new Schema(arrowFields, metadata); + VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator); + + try { + root.allocateNew(); + int rowCount = values.size(); + root.setRowCount(rowCount); + // Write values + for (int fieldIndex = 0; fieldIndex < fields.size(); fieldIndex++) { + VarCharVector vector = (VarCharVector) root.getVector(fieldIndex); + vector.allocateNew(); + + for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) { + ValueRow row = values.get(rowIndex); + if (row != null && row.getColumnsList() != null && + fieldIndex < row.getColumnsList().size()) { + String value = row.getColumns(fieldIndex); + if (value != null) { + // Check byte array size, Arrow buffer size is 32768 bytes + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + vector.setSafe(rowIndex, bytes); + } + } + } + vector.setValueCount(rowCount); + } + return new MetricsData(new ArrowTable(root)); + } catch (Exception e1) { + log.error(e1.getMessage(), e1); + throw e1; + } finally { + root.close(); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + allocator.close(); + throw e; + } + } + + public long getId() { + return Long.parseLong(metadata.getOrDefault(MetricDataConstants.ID, "0")); + } + + public long getTenantId() { + return Long.parseLong(metadata.getOrDefault(MetricDataConstants.TENANT_ID, "0")); + } + + public String getApp() { + return metadata.getOrDefault(MetricDataConstants.APP, ""); + } + + public String getMetrics() { + return metadata.getOrDefault(MetricDataConstants.METRICS, ""); + } + + public int getPriority() { + return Integer.parseInt(metadata.getOrDefault(MetricDataConstants.PRIORITY, "0")); + } + + public long getTime() { + return Long.parseLong(metadata.getOrDefault(MetricDataConstants.TIME, "0")); + } + + public Code getCode() { + return Code.forNumber(Integer.parseInt(metadata.getOrDefault(MetricDataConstants.CODE, "0"))); + } + + public String getMsg() { + return metadata.getOrDefault(MetricDataConstants.MSG, ""); + } + + public String getInstanceName() { + return metadata.getOrDefault(MetricDataConstants.INSTANCE_NAME, null); + } + + public String getInstance() { + return metadata.getOrDefault(MetricDataConstants.INSTANCE, null); + } + + public Map<String, String> getLabels() { + String labelStr = metadata.getOrDefault(MetricDataConstants.LABELS, ""); + if (labelStr == null || "".equals(labelStr)) { + return null; + } + return JsonUtil.fromJson(labelStr, Map.class); + } + + public Map<String, String> getAnnotations() { + String annotationStr = metadata.getOrDefault(MetricDataConstants.ANNOTATIONS, ""); + if (annotationStr == null || "".equals(annotationStr)) { + return null; + } + return JsonUtil.fromJson(annotationStr, Map.class); + } + + public Map<String, String> getMetadata() { + return metadata; + } + + public List<Field> getFieldsList() { + return fields; + } + + public Field getFields(int index) { + return fields.get(index); + } + + public int getFieldsCount() { + return fields.size(); + } + + public List<ValueRow> getValuesList() { + return values; + } + + public ValueRow getValues(int index) { + return values.get(index); + } + + public int getValuesCount() { + return values.size(); + } + + public void addAllFields(List<Field> fieldList) { + fields.addAll(fieldList); + } + + public void clearValues() { + values = new LinkedList<>(); + } + + public void clearFields() { + fields = new LinkedList<>(); + } + } + } + + /** + * Metrics field entity + */ + public static class Field { + /** + * monitoring collect metric field name + */ + private String name; + + /** + * monitoring collect metrics field type, 0-number 1-string + */ + private int type; + + /** + * monitoring collect metrics field unit, % MB GB TB S... + */ + private String unit; + + /** + * is label field + */ + private boolean label; + + public Field() { + name = ""; + unit = ""; + } + + // Getters and setters + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public String getUnit() { + return unit; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public boolean getLabel() { + return label; + } + + public void setLabel(boolean label) { + this.label = label; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public boolean equals(Object o) { + if(!(o instanceof Field)) + return false; + Field other = (Field)o; + return other.name.equals(this.name); + } + + public static class Builder { + private final Field instance; + + private Builder() { + instance = new Field(); + } + + public Builder setName(String name) { + instance.setName(name); + return this; + } + + public Builder setType(int type) { + instance.setType(type); + return this; + } + + public Builder setUnit(String unit) { + instance.setUnit(unit); + return this; + } + + public Builder setLabel(boolean label) { + instance.setLabel(label); + return this; + } + + public Field build() { + return instance; + } + } + } + + /** + * Metrics value row entity + */ + public static class ValueRow { + /** + * monitoring collect metrics value, mapping with the fields + */ + private List<String> columns; + + public ValueRow() { + columns = new LinkedList<>(); + } + + public ValueRow(List<String> columns) { + this.columns = new LinkedList<>(); + this.columns.addAll(columns); + } + + public List<String> getColumnsList() { + return columns; + } + + public void setColumns(List<String> columns) { + this.columns = columns; + } + + public void addColumns(String value) { + if (value == null) { + throw new NullPointerException(); + } + columns.add(value); + } + + public void clearColumns() { + columns = new LinkedList<>(); + } + + public String getColumns(int index) { + return columns.get(index); + } + + public int getColumnsCount() { + return columns.size(); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + private List<String> columns; + + private Builder() { + columns = new LinkedList<>(); + } + + public Builder setColumns(List<String> columns) { + this.columns = columns; + return this; + } + + public Builder addColumn(String column) { + columns.add(column); + return this; + } + + public Builder addAllColumns(Iterable<String> columns) { + for (String column : columns) { + this.columns.add(column); + } + return this; + } + + public ValueRow build() { + return new ValueRow(columns); + } + + public void clear() { + columns.clear(); + } + } + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/plugin/PluginConfig.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/plugin/PluginConfig.java new file mode 100644 index 0000000..cd4b4c1 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/plugin/PluginConfig.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.common.entity.plugin; + +import java.util.ArrayList; +import java.util.List; +import lombok.Data; +import org.apache.hertzbeat.common.entity.job.RuntimeParamDefine; + +/** + * The configuration file of the plugin, including parameters and other information. + */ +@Data +public class PluginConfig { + + private List<RuntimeParamDefine> params; + + public PluginConfig() { + this.params = new ArrayList<>(); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/plugin/PluginContext.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/plugin/PluginContext.java new file mode 100644 index 0000000..4c9fc3f --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/plugin/PluginContext.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.common.entity.plugin; + + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.SerializationUtils; +import org.apache.hertzbeat.common.entity.job.Configmap; + +/** + * plugin context + */ +@Builder +@Slf4j +public class PluginContext { + + /** + * params + */ + private List<Configmap> params; + private ParamHolder paramHolder; + + public ParamHolder param() { + if (paramHolder == null) { + paramHolder = new ParamHolder(params); + } + return paramHolder; + } + + /** + * management parameter operations + */ + public static class ParamHolder { + + private final Map<String, Configmap> paramsMap; + + public ParamHolder(List<Configmap> params) { + if (params == null) { + paramsMap = Collections.emptyMap(); + } else { + this.paramsMap = params.stream().collect(Collectors.toMap(Configmap::getKey, configmap -> configmap)); + } + } + + + /** + * get string param + * + * @param paramName param name; + * @param defaultValue default value + * @return param value + */ + public String getString(String paramName, String defaultValue) { + Configmap configmap = paramsMap.get(paramName); + if (configmap == null) { + return defaultValue; + } + return Optional.ofNullable(configmap.getValue()).map(Object::toString).orElse(defaultValue); + } + + /** + * get int param + * + * @param paramName param name; + * @param defaultValue default value + * @return param value + */ + public Integer getInteger(String paramName, Integer defaultValue) { + Configmap configmap = paramsMap.get(paramName); + if (configmap == null) { + return defaultValue; + } + try { + return Optional.ofNullable(configmap.getValue()).map(v -> Integer.parseInt(v.toString())).orElse(defaultValue); + } catch (Exception e) { + log.warn("int parameter type conversion error paramName:{} value:{} defaultValue:{}", paramName, configmap.getValue(), defaultValue); + return defaultValue; + } + } + + /** + * get boolean param + * + * @param paramName param name; + * @param defaultValue default value + * @return param value + */ + public Boolean getBoolean(String paramName, Boolean defaultValue) { + Configmap configmap = paramsMap.get(paramName); + if (configmap == null) { + return defaultValue; + } + try { + return Optional.ofNullable(configmap.getValue()).map(v -> Boolean.parseBoolean(v.toString())).orElse(defaultValue); + } catch (Exception e) { + log.warn("boolean parameter type conversion error paramName:{} value:{} defaultValue:{}", paramName, configmap.getValue(), defaultValue); + return defaultValue; + } + } + + /** + * get long param + * + * @param paramName param name; + * @param defaultValue default value + * @return param value + */ + public Long getLong(String paramName, Long defaultValue) { + Configmap configmap = paramsMap.get(paramName); + if (configmap == null) { + return defaultValue; + } + try { + return Optional.ofNullable(configmap.getValue()).map(v -> Long.parseLong(v.toString())).orElse(defaultValue); + } catch (Exception e) { + log.warn("long parameter type conversion error paramName:{} value:{} defaultValue:{}", paramName, configmap.getValue(), defaultValue); + return defaultValue; + } + } + + /** + * get all params + * + * @return param list + */ + public List<Configmap> allParams() { + return paramsMap.values().stream().map(SerializationUtils::clone).collect(Collectors.toList()); + } + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java new file mode 100644 index 0000000..3420040 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetricsDto.java @@ -0,0 +1,54 @@ +/* + * 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.push; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * push metrics dto + */ +@Data +@Builder +@AllArgsConstructor +public class PushMetricsDto { + + List<Metrics> metricsList; + + public PushMetricsDto() { + metricsList = new ArrayList<>(); + } + + /** + * metrics + */ + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class Metrics { + private long monitorId; + private Long time; + private List<Map<String, String>> metrics; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/sd/ConnectionConfig.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/sd/ConnectionConfig.java new file mode 100644 index 0000000..369f3aa --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/sd/ConnectionConfig.java @@ -0,0 +1,37 @@ +/* + * 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.sd; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Connection config + */ +@Data +@Builder +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +public class ConnectionConfig { + private String host; + private String port; +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/sd/EurekaDiscoveryResponseEntity.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/sd/EurekaDiscoveryResponseEntity.java new file mode 100644 index 0000000..df316e4 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/sd/EurekaDiscoveryResponseEntity.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.sd; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import tools.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import tools.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import tools.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +/** + * eureka service discovery entity + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JacksonXmlRootElement(localName = "applications") +public class EurekaDiscoveryResponseEntity { + + @JacksonXmlProperty(localName = "versions__delta") + private int versionsDelta; + + @JacksonXmlProperty(localName = "apps__hashcode") + private String appsHashcode; + + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "application") + private List<Application> applications; + + /** + * eureka application entity + */ + @Data + public static class Application { + + @JacksonXmlProperty(localName = "name") + private String name; + + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "instance") + private List<Instance> instances; + } + + /** + * eureka instance entity + */ + @Data + public static class Instance { + @JacksonXmlProperty(localName = "instanceId") + private String instanceId; + + @JacksonXmlProperty(localName = "hostName") + private String hostName; + + @JacksonXmlProperty(localName = "app") + private String app; + + @JacksonXmlProperty(localName = "ipAddr") + private String ipAddr; + + @JacksonXmlProperty(localName = "status") + private String status; + + @JacksonXmlProperty(localName = "port") + private Integer port; + + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/sd/ServiceDiscoveryResponseEntity.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/sd/ServiceDiscoveryResponseEntity.java new file mode 100644 index 0000000..1de4261 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/sd/ServiceDiscoveryResponseEntity.java @@ -0,0 +1,36 @@ +/* + * 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.sd; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * Service Discovery Response Entity + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ServiceDiscoveryResponseEntity { + private List<String> target; +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/queue/CommonDataQueue.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/queue/CommonDataQueue.java new file mode 100644 index 0000000..3b1cb56 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/queue/CommonDataQueue.java @@ -0,0 +1,123 @@ +/* + * 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.queue; + +import java.util.List; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * common data queue + */ +public interface CommonDataQueue { + + /** + * poll collect metrics data for alerter + * @return metrics data + * @throws InterruptedException when poll timeout + */ + CollectRep.MetricsData pollMetricsDataToAlerter() throws InterruptedException; + + /** + * poll collect metrics data for Persistent Storage + * @return metrics data + * @throws InterruptedException when poll timeout + */ + CollectRep.MetricsData pollMetricsDataToStorage() throws InterruptedException; + + /** + * poll service discovery data + * @return metrics data + * @throws InterruptedException when poll timeout + */ + CollectRep.MetricsData pollServiceDiscoveryData() throws InterruptedException; + + /** + * send collect metrics data + * @param metricsData metrics data + */ + void sendMetricsData(CollectRep.MetricsData metricsData); + + /** + * send metrics data to storage from alerter + * @param metricsData metrics data + */ + void sendMetricsDataToStorage(CollectRep.MetricsData metricsData); + + /** + * send service discovery data + * @param metricsData service discovery data + */ + void sendServiceDiscoveryData(CollectRep.MetricsData metricsData); + + /** + * send log entry to queue + * @param logEntry log entry data based on OpenTelemetry log data model + * @throws InterruptedException when sending is interrupted + */ + void sendLogEntry(LogEntry logEntry); + + /** + * poll log entry from queue + * @return log entry data + * @throws InterruptedException when poll timeout + */ + LogEntry pollLogEntry() throws InterruptedException; + + /** + * send log entry to storage queue + * @param logEntry log entry data based on OpenTelemetry log data model + * @throws InterruptedException when sending is interrupted + */ + void sendLogEntryToStorage(LogEntry logEntry); + + /** + * poll log entry from storage queue + * @return log entry data + * @throws InterruptedException when poll timeout + */ + LogEntry pollLogEntryToStorage() throws InterruptedException; + + /** + * send batch log entries to alert queue + * @param logEntries list of log entry data + */ + void sendLogEntryToAlertBatch(List<LogEntry> logEntries); + + /** + * poll batch log entries from alert queue + * @param maxBatchSize maximum number of entries to poll + * @return list of log entry data + * @throws InterruptedException when poll timeout + */ + List<LogEntry> pollLogEntryToAlertBatch(int maxBatchSize) throws InterruptedException; + + /** + * send batch log entries to storage queue + * @param logEntries list of log entry data + */ + void sendLogEntryToStorageBatch(List<LogEntry> logEntries); + + /** + * poll batch log entries from storage queue + * @param maxBatchSize maximum number of entries to poll + * @return list of log entry data + * @throws InterruptedException when poll timeout + */ + List<LogEntry> pollLogEntryToStorageBatch(int maxBatchSize) throws InterruptedException; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/KafkaLogEntryDeserializer.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/KafkaLogEntryDeserializer.java new file mode 100644 index 0000000..bf86a48 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/KafkaLogEntryDeserializer.java @@ -0,0 +1,62 @@ +/* + * 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.serialize; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.kafka.common.header.Headers; +import org.apache.kafka.common.serialization.Deserializer; + +/** + * Kafka LogEntry deserializer using JSON + */ +@Slf4j +public class KafkaLogEntryDeserializer implements Deserializer<LogEntry> { + + @Override + public void configure(Map<String, ?> configs, boolean isKey) { + Deserializer.super.configure(configs, isKey); + } + + @Override + public LogEntry deserialize(String topic, byte[] data) { + if (data == null || data.length == 0) { + log.warn("Empty data received for topic: {}", topic); + return null; + } + String jsonString = new String(data, StandardCharsets.UTF_8); + LogEntry logEntry = JsonUtil.fromJson(jsonString, LogEntry.class); + if (logEntry == null) { + log.warn("Failed to deserialize LogEntry from topic: {}", topic); + } + return logEntry; + } + + @Override + public LogEntry deserialize(String topic, Headers headers, byte[] data) { + return deserialize(topic, data); + } + + @Override + public void close() { + Deserializer.super.close(); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/KafkaLogEntrySerializer.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/KafkaLogEntrySerializer.java new file mode 100644 index 0000000..929b6d7 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/KafkaLogEntrySerializer.java @@ -0,0 +1,62 @@ +/* + * 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.serialize; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.kafka.common.header.Headers; +import org.apache.kafka.common.serialization.Serializer; + +/** + * Kafka LogEntry serializer using JSON + */ +@Slf4j +public class KafkaLogEntrySerializer implements Serializer<LogEntry> { + + @Override + public void configure(Map<String, ?> configs, boolean isKey) { + Serializer.super.configure(configs, isKey); + } + + @Override + public byte[] serialize(String topic, LogEntry logEntry) { + if (logEntry == null) { + log.warn("LogEntry is null for topic: {}", topic); + return null; + } + String jsonString = JsonUtil.toJson(logEntry); + if (jsonString == null) { + log.error("Failed to serialize LogEntry for topic: {}", topic); + return null; + } + return jsonString.getBytes(StandardCharsets.UTF_8); + } + + @Override + public byte[] serialize(String topic, Headers headers, LogEntry logEntry) { + return serialize(topic, logEntry); + } + + @Override + public void close() { + Serializer.super.close(); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/KafkaMetricsDataDeserializer.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/KafkaMetricsDataDeserializer.java new file mode 100644 index 0000000..3d4b95b --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/KafkaMetricsDataDeserializer.java @@ -0,0 +1,63 @@ +/* + * 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.serialize; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import java.util.Map; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.ipc.ArrowStreamReader; +import org.apache.arrow.vector.table.ArrowTable; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.kafka.common.header.Headers; +import org.apache.kafka.common.serialization.Deserializer; + +/** + * kafka metrics data deserializer + */ +public class KafkaMetricsDataDeserializer implements Deserializer<CollectRep.MetricsData> { + + @Override + public void configure(Map<String, ?> configs, boolean isKey) { + Deserializer.super.configure(configs, isKey); + } + + @Override + public CollectRep.MetricsData deserialize(String s, byte[] bytes){ + try (ByteArrayInputStream in = new ByteArrayInputStream(bytes); + ArrowStreamReader reader = new ArrowStreamReader(Channels.newChannel(in), new RootAllocator())) { + VectorSchemaRoot root = reader.getVectorSchemaRoot(); + reader.loadNextBatch(); + return new CollectRep.MetricsData(new ArrowTable(root)); + } catch (IOException e) { + throw new RuntimeException("Failed to deserialize Arrow table", e); + } + } + + @Override + public CollectRep.MetricsData deserialize(String topic, Headers headers, byte[] data) { + return Deserializer.super.deserialize(topic, headers, data); + } + + @Override + public void close() { + Deserializer.super.close(); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/KafkaMetricsDataSerializer.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/KafkaMetricsDataSerializer.java new file mode 100644 index 0000000..39f9c4c --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/KafkaMetricsDataSerializer.java @@ -0,0 +1,73 @@ +/* + * 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.serialize; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.ipc.ArrowStreamWriter; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.kafka.common.header.Headers; +import org.apache.kafka.common.serialization.Serializer; + +/** + * kafka metrics data serializer + */ + +@Slf4j +public class KafkaMetricsDataSerializer implements Serializer<CollectRep.MetricsData> { + + @Override + public void configure(Map<String, ?> configs, boolean isKey) { + Serializer.super.configure(configs, isKey); + } + + @Override + public byte[] serialize(String s, CollectRep.MetricsData metricsData) { + // todo use the ArrowTable bytebuffer to direct send zero copy + if (metricsData == null) { + log.error("metricsData is null"); + return null; + } + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + VectorSchemaRoot root = metricsData.toVectorSchemaRootAndRelease(); + ArrowStreamWriter writer = new ArrowStreamWriter(root, + null, Channels.newChannel(out))) { + writer.start(); + writer.writeBatch(); + writer.end(); + return out.toByteArray(); + } catch (IOException e) { + log.error("sendMetricsData error", e); + } + return null; + } + + @Override + public byte[] serialize(String topic, Headers headers, CollectRep.MetricsData data) { + return Serializer.super.serialize(topic, headers, data); + } + + @Override + public void close() { + Serializer.super.close(); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/RedisLogEntryCodec.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/RedisLogEntryCodec.java new file mode 100644 index 0000000..f8d84b3 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/RedisLogEntryCodec.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.serialize; + +import io.lettuce.core.codec.RedisCodec; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.util.JsonUtil; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +/** + * Redis LogEntry codec using JSON serialization + */ +@Slf4j +public class RedisLogEntryCodec implements RedisCodec<String, LogEntry> { + + @Override + public String decodeKey(ByteBuffer byteBuffer) { + return Unpooled.wrappedBuffer(byteBuffer).toString(StandardCharsets.UTF_8); + } + + @Override + public LogEntry decodeValue(ByteBuffer byteBuffer) { + if (byteBuffer == null || !byteBuffer.hasRemaining()) { + return null; + } + String jsonString = Unpooled.wrappedBuffer(byteBuffer).toString(StandardCharsets.UTF_8); + LogEntry logEntry = JsonUtil.fromJson(jsonString, LogEntry.class); + if (logEntry == null) { + log.error("Failed to decode LogEntry from JSON"); + } + return logEntry; + } + + @Override + public ByteBuffer encodeKey(String s) { + return ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public ByteBuffer encodeValue(LogEntry logEntry) { + String jsonString = JsonUtil.toJson(logEntry); + if (jsonString == null) { + log.error("Failed to encode LogEntry to JSON"); + return null; + } + return ByteBuffer.wrap(jsonString.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/RedisMetricsDataCodec.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/RedisMetricsDataCodec.java new file mode 100644 index 0000000..37c5ff3 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/serialize/RedisMetricsDataCodec.java @@ -0,0 +1,106 @@ +/* + * 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.serialize; + +import io.lettuce.core.codec.RedisCodec; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.ipc.ArrowStreamReader; +import org.apache.arrow.vector.ipc.ArrowStreamWriter; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * redis metrics data codec + */ +@Slf4j +public class RedisMetricsDataCodec implements RedisCodec<String, CollectRep.MetricsData> { + + private final BufferAllocator allocator; + + public RedisMetricsDataCodec() { + this.allocator = new RootAllocator(); + } + + @Override + public String decodeKey(ByteBuffer byteBuffer) { + return Unpooled.wrappedBuffer(byteBuffer).toString(StandardCharsets.UTF_8); + } + + @Override + public CollectRep.MetricsData decodeValue(ByteBuffer byteBuffer) { + if (byteBuffer == null || !byteBuffer.hasRemaining()) { + return null; + } + try { + byte[] bytes; + if (byteBuffer.hasArray()) { + bytes = Arrays.copyOfRange(byteBuffer.array(), + byteBuffer.position(), byteBuffer.limit()); + } else { + bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + } + try (ByteArrayInputStream in = new ByteArrayInputStream(bytes); + ArrowStreamReader reader = new ArrowStreamReader( + Channels.newChannel(in), allocator)) { + reader.loadNextBatch(); + VectorSchemaRoot root = reader.getVectorSchemaRoot(); + if (root == null || root.getRowCount() == 0) { + log.warn("Empty data received"); + return null; + } + return new CollectRep.MetricsData(root); + } + } catch (Exception e) { + log.error("Failed to decode metrics data", e); + return null; + } + } + + @Override + public ByteBuffer encodeKey(String s) { + return ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public ByteBuffer encodeValue(CollectRep.MetricsData metricsData) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + VectorSchemaRoot root = metricsData.toVectorSchemaRootAndRelease(); + ArrowStreamWriter writer = new ArrowStreamWriter(root, + null, Channels.newChannel(out))) { + writer.start(); + writer.writeBatch(); + writer.end(); + return ByteBuffer.wrap(out.toByteArray()); + } catch (IOException e) { + log.error("sendMetricsData error", e); + } + return null; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8Control.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8Control.java new file mode 100644 index 0000000..a3065fb --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8Control.java @@ -0,0 +1,113 @@ +/* + * 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.support; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; + +/** + * i18n resource bundle control + */ + +public class ResourceBundleUtf8Control extends ResourceBundle.Control { + + private static final String JAVA_CLASS = "java.class"; + private static final String JAVA_PROPERTIES = "java.properties"; + private static final String SPILT = "://"; + + @Override + public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) + throws IllegalAccessException, InstantiationException, IOException { + + String bundleName = toBundleName(baseName, locale); + ResourceBundle bundle = null; + if (JAVA_CLASS.equals(format)) { + try { + @SuppressWarnings("unchecked") + Class<? extends ResourceBundle> bundleClass = + (Class<? extends ResourceBundle>) loader.loadClass(bundleName); + + // If the class isn't a ResourceBundle subclass, throw a + // ClassCastException. + if (ResourceBundle.class.isAssignableFrom(bundleClass)) { + bundle = bundleClass.getDeclaredConstructor().newInstance(); + } else { + throw new ClassCastException(bundleClass.getName() + + " cannot be cast to ResourceBundle"); + } + } catch (ClassNotFoundException ignored) {} + catch (InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } else if (JAVA_PROPERTIES.equals(format)) { + final String resourceName = toResourceName0(bundleName); + if (resourceName == null) { + return null; + } + InputStream stream = getResourceInputStream(loader, resourceName, reload); + + if (stream != null) { + try (stream) { + bundle = new PropertyResourceBundle(new InputStreamReader(stream, StandardCharsets.UTF_8)); + } + } + } else { + throw new IllegalArgumentException("unknown format: " + format); + } + return bundle; + } + + private String toResourceName0(String bundleName) { + // application protocol check + if (bundleName.contains(SPILT)) { + return null; + } else { + return toResourceName(bundleName, "properties"); + } + } + + private InputStream getResourceInputStream(ClassLoader classLoader, String resourceName, boolean reloadFlag) throws IOException { + + InputStream is = null; + + if (reloadFlag) { + URL url = classLoader.getResource(resourceName); + if (url != null) { + URLConnection connection = url.openConnection(); + if (connection != null) { + // Disable caches to get fresh data for reloading. + connection.setUseCaches(false); + is = connection.getInputStream(); + } + } + } else { + is = classLoader.getResourceAsStream(resourceName); + } + + return is; + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/AlertExpressionException.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/AlertExpressionException.java new file mode 100644 index 0000000..c3ab443 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/AlertExpressionException.java @@ -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.support.exception; + +/** + * Alert expression exception + */ +public class AlertExpressionException extends RuntimeException { + + public AlertExpressionException(String message) { + super(message); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/CommonDataQueueUnknownException.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/CommonDataQueueUnknownException.java new file mode 100644 index 0000000..2458e8e --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/CommonDataQueueUnknownException.java @@ -0,0 +1,39 @@ +/* + * 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.support.exception; + +/** + * Common data queue unknown exception + */ +public class CommonDataQueueUnknownException extends RuntimeException { + + public CommonDataQueueUnknownException() { + } + + public CommonDataQueueUnknownException(String message) { + super(message); + } + + public CommonDataQueueUnknownException(String message, Throwable cause) { + super(message, cause); + } + + public CommonDataQueueUnknownException(Throwable cause) { + super(cause); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/CommonException.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/CommonException.java new file mode 100644 index 0000000..a0961bd --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/CommonException.java @@ -0,0 +1,27 @@ +/* + * 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.support.exception; + +/** + * common exception + */ +public class CommonException extends RuntimeException { + public CommonException(String message) { + super(message); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/ExpressionVisitorException.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/ExpressionVisitorException.java new file mode 100644 index 0000000..d161d55 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/ExpressionVisitorException.java @@ -0,0 +1,29 @@ +/* + * 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.support.exception; + +/** + * Expression visitor exception + */ +public class ExpressionVisitorException extends RuntimeException { + + + public ExpressionVisitorException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/IgnoreException.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/IgnoreException.java new file mode 100644 index 0000000..2d21b60 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/IgnoreException.java @@ -0,0 +1,27 @@ +/* + * 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.support.exception; + +/** + * normal ignore + */ +public class IgnoreException extends RuntimeException { + public IgnoreException(String message) { + super(message); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/SendMessageException.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/SendMessageException.java new file mode 100644 index 0000000..7b74593 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/exception/SendMessageException.java @@ -0,0 +1,27 @@ +/* + * 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.support.exception; + +/** + * send message exception + */ +public class SendMessageException extends RuntimeException { + public SendMessageException(String message) { + super(message); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityException.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityException.java new file mode 100644 index 0000000..d343cb7 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityException.java @@ -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. + */ + +package org.apache.hertzbeat.common.support.valid; + +/** + * SQL security validation exception + */ +public class SqlSecurityException extends RuntimeException { + + public SqlSecurityException(String message) { + super(message); + } + + public SqlSecurityException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/timer/HashedWheelTimer.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/timer/HashedWheelTimer.java new file mode 100644 index 0000000..cb1801f --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/timer/HashedWheelTimer.java @@ -0,0 +1,801 @@ +/* + * 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.timer; + +import org.apache.hertzbeat.common.util.NetworkUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Derived from Apache Dubbo org.apache.dubbo.common.timer.HashedWheelTimer + * <a href="https://github.com/apache/dubbo/blob/3.2/dubbo-common/src/main/java/org/apache/dubbo/common/timer/HashedWheelTimer.java">HashedWheelTimer.java</a> + * A {@link Timer} optimized for approximated I/O timeout scheduling. + * + * <h3>Tick Duration</h3> + * <p> + * As described with 'approximated', this timer does not execute the scheduled + * {@link TimerTask} on time. {@link HashedWheelTimer}, on every tick, will + * check if there are any {@link TimerTask}s behind the schedule and execute + * them. + * <p> + * You can increase or decrease the accuracy of the execution timing by + * specifying smaller or larger tick duration in the constructor. In most + * network applications, I/O timeout does not need to be accurate. Therefore, + * the default tick duration is 100 milliseconds and you will not need to try + * different configurations in most cases. + * + * <h3>Ticks per Wheel (Wheel Size)</h3> + * <p> + * {@link HashedWheelTimer} maintains a data structure called 'wheel'. + * To put simply, a wheel is a hash table of {@link TimerTask}s whose hash + * function is 'dead line of the task'. The default number of ticks per wheel + * (i.e. the size of the wheel) is 512. You could specify a larger value + * if you are going to schedule a lot of timeouts. + * + * <h3>Do not create many instances.</h3> + * <p> + * {@link HashedWheelTimer} creates a new thread whenever it is instantiated and + * started. Therefore, you should make sure to create only one instance and + * share it across your application. One of the common mistakes, that makes + * your application unresponsive, is to create a new instance for every connection. + * + * <h3>Implementation Details</h3> + * <p> + * {@link HashedWheelTimer} is based on + * <a href="http://cseweb.ucsd.edu/users/varghese/">George Varghese</a> and + * Tony Lauck's paper, + * <a href="http://cseweb.ucsd.edu/users/varghese/PAPERS/twheel.ps.Z">'Hashed + * and Hierarchical Timing Wheels: data structures to efficiently implement a + * timer facility'</a>. More comprehensive slides are located + * <a href="http://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt">here</a>. + */ +@SuppressWarnings("PMD") +public class HashedWheelTimer implements Timer { + + private static final Logger logger = LoggerFactory.getLogger(HashedWheelTimer.class); + + private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(); + private static final AtomicBoolean WARNED_TOO_MANY_INSTANCES = new AtomicBoolean(); + private static final int INSTANCE_COUNT_LIMIT = 64; + private static final AtomicIntegerFieldUpdater<HashedWheelTimer> WORKER_STATE_UPDATER = + AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimer.class, "workerState"); + + private final Worker worker = new Worker(); + private final Thread workerThread; + + private static final int WORKER_STATE_INIT = 0; + private static final int WORKER_STATE_STARTED = 1; + private static final int WORKER_STATE_SHUTDOWN = 2; + + /** + * 0 - init, 1 - started, 2 - shut down + */ + @SuppressWarnings({"unused", "FieldMayBeFinal"}) + private volatile int workerState; + + private final long tickDuration; + private final HashedWheelBucket[] wheel; + private final int mask; + private final CountDownLatch startTimeInitialized = new CountDownLatch(1); + private final Queue<HashedWheelTimeout> timeouts = new LinkedBlockingQueue<>(); + private final Queue<HashedWheelTimeout> cancelledTimeouts = new LinkedBlockingQueue<>(); + private final AtomicLong pendingTimeouts = new AtomicLong(0); + private final long maxPendingTimeouts; + + private volatile long startTime; + + /** + * Creates a new timer with the default thread factory + * ({@link Executors#defaultThreadFactory()}), default tick duration, and + * default number of ticks per wheel. + */ + public HashedWheelTimer() { + this(Executors.defaultThreadFactory()); + } + + /** + * Creates a new timer with the default thread factory + * ({@link Executors#defaultThreadFactory()}) and default number of ticks + * per wheel. + * + * @param tickDuration the duration between tick + * @param unit the time unit of the {@code tickDuration} + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code tickDuration} is <= 0 + */ + public HashedWheelTimer(long tickDuration, TimeUnit unit) { + this(Executors.defaultThreadFactory(), tickDuration, unit); + } + + /** + * Creates a new timer with the default thread factory + * ({@link Executors#defaultThreadFactory()}). + * + * @param tickDuration the duration between tick + * @param unit the time unit of the {@code tickDuration} + * @param ticksPerWheel the size of the wheel + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if either of {@code tickDuration} and {@code ticksPerWheel} is <= 0 + */ + public HashedWheelTimer(long tickDuration, TimeUnit unit, int ticksPerWheel) { + this(Executors.defaultThreadFactory(), tickDuration, unit, ticksPerWheel); + } + + /** + * Creates a new timer with the default tick duration and default number of + * ticks per wheel. + * + * @param threadFactory a {@link ThreadFactory} that creates a + * background {@link Thread} which is dedicated to + * {@link TimerTask} execution. + * @throws NullPointerException if {@code threadFactory} is {@code null} + */ + public HashedWheelTimer(ThreadFactory threadFactory) { + this(threadFactory, 100, TimeUnit.MILLISECONDS); + } + + /** + * Creates a new timer with the default number of ticks per wheel. + * + * @param threadFactory a {@link ThreadFactory} that creates a + * background {@link Thread} which is dedicated to + * {@link TimerTask} execution. + * @param tickDuration the duration between tick + * @param unit the time unit of the {@code tickDuration} + * @throws NullPointerException if either of {@code threadFactory} and {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code tickDuration} is <= 0 + */ + public HashedWheelTimer( + ThreadFactory threadFactory, long tickDuration, TimeUnit unit) { + this(threadFactory, tickDuration, unit, 512); + } + + /** + * Creates a new timer. + * + * @param threadFactory a {@link ThreadFactory} that creates a + * background {@link Thread} which is dedicated to + * {@link TimerTask} execution. + * @param tickDuration the duration between tick + * @param unit the time unit of the {@code tickDuration} + * @param ticksPerWheel the size of the wheel + * @throws NullPointerException if either of {@code threadFactory} and {@code unit} is {@code null} + * @throws IllegalArgumentException if either of {@code tickDuration} and {@code ticksPerWheel} is <= 0 + */ + public HashedWheelTimer( + ThreadFactory threadFactory, + long tickDuration, TimeUnit unit, int ticksPerWheel) { + this(threadFactory, tickDuration, unit, ticksPerWheel, -1); + } + + /** + * Creates a new timer. + * + * @param threadFactory a {@link ThreadFactory} that creates a + * background {@link Thread} which is dedicated to + * {@link TimerTask} execution. + * @param tickDuration the duration between tick + * @param unit the time unit of the {@code tickDuration} + * @param ticksPerWheel the size of the wheel + * @param maxPendingTimeouts The maximum number of pending timeouts after which call to + * {@code newTimeout} will result in + * {@link RejectedExecutionException} + * being thrown. No maximum pending timeouts limit is assumed if + * this value is 0 or negative. + * @throws NullPointerException if either of {@code threadFactory} and {@code unit} is {@code null} + * @throws IllegalArgumentException if either of {@code tickDuration} and {@code ticksPerWheel} is <= 0 + */ + public HashedWheelTimer( + ThreadFactory threadFactory, + long tickDuration, TimeUnit unit, int ticksPerWheel, + long maxPendingTimeouts) { + + if (threadFactory == null) { + throw new NullPointerException("threadFactory"); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + if (tickDuration <= 0) { + throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration); + } + if (ticksPerWheel <= 0) { + throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel); + } + + // Normalize ticksPerWheel to power of two and initialize the wheel. + wheel = createWheel(ticksPerWheel); + mask = wheel.length - 1; + + // Convert tickDuration to nanos. + this.tickDuration = unit.toNanos(tickDuration); + + // Prevent overflow. + if (this.tickDuration >= Long.MAX_VALUE / wheel.length) { + throw new IllegalArgumentException(String.format( + "tickDuration: %d (expected: 0 < tickDuration in nanos < %d", + tickDuration, Long.MAX_VALUE / wheel.length)); + } + workerThread = threadFactory.newThread(worker); + + this.maxPendingTimeouts = maxPendingTimeouts; + + if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT + && WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) { + reportTooManyInstances(); + } + } + + @Override + protected void finalize() throws Throwable { + try { + super.finalize(); + } finally { + // This object is going to be GCed and it is assumed the ship has sailed to do a proper shutdown. If + // we have not yet shutdown then we want to make sure we decrement the active instance count. + if (WORKER_STATE_UPDATER.getAndSet(this, WORKER_STATE_SHUTDOWN) != WORKER_STATE_SHUTDOWN) { + INSTANCE_COUNTER.decrementAndGet(); + } + } + } + + private static HashedWheelBucket[] createWheel(int ticksPerWheel) { + if (ticksPerWheel <= 0) { + throw new IllegalArgumentException( + "ticksPerWheel must be greater than 0: " + ticksPerWheel); + } + if (ticksPerWheel > 1073741824) { + throw new IllegalArgumentException( + "ticksPerWheel may not be greater than 2^30: " + ticksPerWheel); + } + + ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel); + HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel]; + for (int i = 0; i < wheel.length; i++) { + wheel[i] = new HashedWheelBucket(); + } + return wheel; + } + + private static int normalizeTicksPerWheel(int ticksPerWheel) { + int normalizedTicksPerWheel = ticksPerWheel - 1; + normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 1; + normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 2; + normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 4; + normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 8; + normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 16; + return normalizedTicksPerWheel + 1; + } + + /** + * Starts the background thread explicitly. The background thread will + * start automatically on demand even if you did not call this method. + * + * @throws IllegalStateException if this timer has been + * {@linkplain #stop() stopped} already + */ + public void start() { + switch (WORKER_STATE_UPDATER.get(this)) { + case WORKER_STATE_INIT: + if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) { + workerThread.start(); + } + break; + case WORKER_STATE_STARTED: + break; + case WORKER_STATE_SHUTDOWN: + throw new IllegalStateException("cannot be started once stopped"); + default: + throw new Error("Invalid WorkerState"); + } + + // Wait until the startTime is initialized by the worker. + while (startTime == 0) { + try { + startTimeInitialized.await(); + } catch (InterruptedException ignore) { + // Ignore - it will be ready very soon. + } + } + } + + @Override + public Set<Timeout> stop() { + if (Thread.currentThread() == workerThread) { + throw new IllegalStateException( + HashedWheelTimer.class.getSimpleName() + + ".stop() cannot be called from " + + TimerTask.class.getSimpleName()); + } + + if (!WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_STARTED, WORKER_STATE_SHUTDOWN)) { + // workerState can be 0 or 2 at this moment - let it always be 2. + if (WORKER_STATE_UPDATER.getAndSet(this, WORKER_STATE_SHUTDOWN) != WORKER_STATE_SHUTDOWN) { + INSTANCE_COUNTER.decrementAndGet(); + } + + return Collections.emptySet(); + } + + try { + boolean interrupted = false; + while (workerThread.isAlive()) { + workerThread.interrupt(); + try { + workerThread.join(100); + } catch (InterruptedException ignored) { + interrupted = true; + } + } + + if (interrupted) { + Thread.currentThread().interrupt(); + } + } finally { + INSTANCE_COUNTER.decrementAndGet(); + } + return worker.unprocessedTimeouts(); + } + + @Override + public boolean isStop() { + return WORKER_STATE_SHUTDOWN == WORKER_STATE_UPDATER.get(this); + } + + @Override + public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) { + if (task == null) { + throw new NullPointerException("task"); + } + if (unit == null) { + throw new NullPointerException("unit"); + } + + long pendingTimeoutsCount = pendingTimeouts.incrementAndGet(); + + if (maxPendingTimeouts > 0 && pendingTimeoutsCount > maxPendingTimeouts) { + pendingTimeouts.decrementAndGet(); + throw new RejectedExecutionException("Number of pending timeouts (" + + pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending " + + "timeouts (" + maxPendingTimeouts + ")"); + } + + start(); + + // Add the timeout to the timeout queue which will be processed on the next tick. + // During processing all the queued HashedWheelTimeouts will be added to the correct HashedWheelBucket. + long deadline = System.nanoTime() + unit.toNanos(delay) - startTime; + + // Guard against overflow. + if (delay > 0 && deadline < 0) { + deadline = Long.MAX_VALUE; + } + HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline); + timeouts.add(timeout); + return timeout; + } + + /** + * Returns the number of pending timeouts of this {@link Timer}. + */ + public long pendingTimeouts() { + return pendingTimeouts.get(); + } + + private static void reportTooManyInstances() { + logger.error("You are creating too many HashedWheelTimer instances. " + + "HashedWheelTimer is a shared resource that must be reused across the JVM," + + "so that only a few instances are created."); + } + + private final class Worker implements Runnable { + private final Set<Timeout> unprocessedTimeouts = new HashSet<>(); + + private long tick; + + @Override + public void run() { + // Initialize the startTime. + startTime = System.nanoTime(); + if (startTime == 0) { + // We use 0 as an metric for the uninitialized value here, so make sure it's not 0 when initialized. + startTime = 1; + } + + // Notify the other threads waiting for the initialization at start(). + startTimeInitialized.countDown(); + + do { + final long deadline = waitForNextTick(); + if (deadline > 0) { + int idx = (int) (tick & mask); + processCancelledTasks(); + HashedWheelBucket bucket = + wheel[idx]; + transferTimeoutsToBuckets(); + bucket.expireTimeouts(deadline); + tick++; + } + } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED); + + // Fill the unprocessedTimeouts so we can return them from stop() method. + for (HashedWheelBucket bucket : wheel) { + bucket.clearTimeouts(unprocessedTimeouts); + } + for (; ; ) { + HashedWheelTimeout timeout = timeouts.poll(); + if (timeout == null) { + break; + } + if (!timeout.isCancelled()) { + unprocessedTimeouts.add(timeout); + } + } + processCancelledTasks(); + } + + private void transferTimeoutsToBuckets() { + // transfer only max. 100000 timeouts per tick to prevent a thread to stale the workerThread when it just + // adds new timeouts in a loop. + for (int i = 0; i < 100000; i++) { + HashedWheelTimeout timeout = timeouts.poll(); + if (timeout == null) { + // all processed + break; + } + if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) { + // Was cancelled in the meantime. + continue; + } + + long calculated = timeout.deadline / tickDuration; + timeout.remainingRounds = (calculated - tick) / wheel.length; + + // Ensure we don't schedule for past. + final long ticks = Math.max(calculated, tick); + int stopIndex = (int) (ticks & mask); + + HashedWheelBucket bucket = wheel[stopIndex]; + bucket.addTimeout(timeout); + } + } + + private void processCancelledTasks() { + for (; ; ) { + HashedWheelTimeout timeout = cancelledTimeouts.poll(); + if (timeout == null) { + // all processed + break; + } + try { + timeout.remove(); + } catch (Throwable t) { + if (logger.isWarnEnabled()) { + logger.warn("An exception was thrown while process a cancellation task", t); + } + } + } + } + + /** + * calculate goal nanoTime from startTime and current tick number, + * then wait until that goal has been reached. + * + * @return Long.MIN_VALUE if received a shutdown request, + * current time otherwise (with Long.MIN_VALUE changed by +1) + */ + private long waitForNextTick() { + long deadline = tickDuration * (tick + 1); + + for (; ; ) { + final long currentTime = System.nanoTime() - startTime; + long sleepTimeMs = (deadline - currentTime + 999999) / 1000000; + + if (sleepTimeMs <= 0) { + if (currentTime == Long.MIN_VALUE) { + return -Long.MAX_VALUE; + } else { + return currentTime; + } + } + if (NetworkUtil.isWindowsPlatform()) { + sleepTimeMs = sleepTimeMs / 10 * 10; + } + + try { + Thread.sleep(sleepTimeMs); + } catch (InterruptedException ignored) { + if (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) { + return Long.MIN_VALUE; + } + } + } + } + + Set<Timeout> unprocessedTimeouts() { + return Collections.unmodifiableSet(unprocessedTimeouts); + } + } + + private static final class HashedWheelTimeout implements Timeout { + + private static final int ST_INIT = 0; + private static final int ST_CANCELLED = 1; + private static final int ST_EXPIRED = 2; + private static final AtomicIntegerFieldUpdater<HashedWheelTimeout> STATE_UPDATER = + AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimeout.class, "state"); + + private final HashedWheelTimer timer; + private final TimerTask task; + private final long deadline; + + @SuppressWarnings({"unused", "FieldMayBeFinal", "RedundantFieldInitialization"}) + private volatile int state = ST_INIT; + + /** + * RemainingRounds will be calculated and set by Worker.transferTimeoutsToBuckets() before the + * HashedWheelTimeout will be added to the correct HashedWheelBucket. + */ + long remainingRounds; + + /** + * This will be used to chain timeouts in HashedWheelTimerBucket via a double-linked-list. + * As only the workerThread will act on it there is no need for synchronization / volatile. + */ + HashedWheelTimeout next; + HashedWheelTimeout prev; + + /** + * The bucket to which the timeout was added + */ + HashedWheelBucket bucket; + + HashedWheelTimeout(HashedWheelTimer timer, TimerTask task, long deadline) { + this.timer = timer; + this.task = task; + this.deadline = deadline; + } + + @Override + public Timer timer() { + return timer; + } + + @Override + public TimerTask task() { + return task; + } + + @Override + public boolean cancel() { + // only update the state it will be removed from HashedWheelBucket on next tick. + if (!compareAndSetState(ST_INIT, ST_CANCELLED)) { + return false; + } + // If a task should be canceled we put this to another queue which will be processed on each tick. + // So this means that we will have a GC latency of max. 1 tick duration which is good enough. This way + // we can make again use of our MpscLinkedQueue and so minimize the locking / overhead as much as possible. + timer.cancelledTimeouts.add(this); + return true; + } + + void remove() { + HashedWheelBucket bucket = this.bucket; + if (bucket != null) { + bucket.remove(this); + } + timer.pendingTimeouts.decrementAndGet(); + } + + public boolean compareAndSetState(int expected, int state) { + return STATE_UPDATER.compareAndSet(this, expected, state); + } + + public int state() { + return state; + } + + @Override + public boolean isCancelled() { + return state() == ST_CANCELLED; + } + + @Override + public boolean isExpired() { + return state() == ST_EXPIRED; + } + + public void expire() { + if (!compareAndSetState(ST_INIT, ST_EXPIRED)) { + return; + } + + try { + remove(); + task.run(this); + } catch (Throwable t) { + if (logger.isWarnEnabled()) { + logger.warn("An exception was thrown by {}.", TimerTask.class.getSimpleName(), t); + } + } + } + + @Override + public String toString() { + final long currentTime = System.nanoTime(); + long remaining = deadline - currentTime + timer.startTime; + + StringBuilder buf = new StringBuilder(192) + .append("HashedWheelTimer") + .append('(') + .append("deadline: "); + if (remaining > 0) { + buf.append(remaining) + .append(" ns later"); + } else if (remaining < 0) { + buf.append(-remaining) + .append(" ns ago"); + } else { + buf.append("now"); + } + + if (isCancelled()) { + buf.append(", cancelled"); + } + + return buf.append(", task: ") + .append(task()) + .append(')') + .toString(); + } + } + + /** + * Bucket that stores HashedWheelTimeouts. These are stored in a linked-list like datastructure to allow easy + * removal of HashedWheelTimeouts in the middle. Also the HashedWheelTimeout act as nodes themself and so no + * extra object creation is needed. + */ + private static final class HashedWheelBucket { + + /** + * Used for the linked-list datastructure + */ + private HashedWheelTimeout head; + private HashedWheelTimeout tail; + + /** + * Add {@link HashedWheelTimeout} to this bucket. + */ + void addTimeout(HashedWheelTimeout timeout) { + assert timeout.bucket == null; + timeout.bucket = this; + if (head == null) { + head = tail = timeout; + } else { + tail.next = timeout; + timeout.prev = tail; + tail = timeout; + } + } + + /** + * Expire all {@link HashedWheelTimeout}s for the given {@code deadline}. + */ + void expireTimeouts(long deadline) { + HashedWheelTimeout timeout = head; + + // process all timeouts + while (timeout != null) { + HashedWheelTimeout next = timeout.next; + if (timeout.remainingRounds <= 0) { + if (timeout.deadline <= deadline) { + timeout.expire(); + } else { + // The timeout was placed into a wrong slot. This should never happen. + throw new IllegalStateException(String.format( + "timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline)); + } + } else if (!timeout.isCancelled()) { + timeout.remainingRounds--; + } + timeout = next; + } + } + + public HashedWheelTimeout remove(HashedWheelTimeout timeout) { + HashedWheelTimeout next = timeout.next; + // remove timeout that was either processed or cancelled by updating the linked-list + if (timeout.prev != null) { + timeout.prev.next = next; + } + if (timeout.next != null) { + timeout.next.prev = timeout.prev; + } + + if (timeout == head) { + // if timeout is also the tail we need to adjust the entry too + if (timeout == tail) { + tail = null; + head = null; + } else { + head = next; + } + } else if (timeout == tail) { + // if the timeout is the tail modify the tail to be the prev node. + tail = timeout.prev; + } + // null out prev, next and bucket to allow for GC. + timeout.prev = null; + timeout.next = null; + timeout.bucket = null; + return next; + } + + /** + * Clear this bucket and return all not expired / cancelled {@link Timeout}s. + */ + void clearTimeouts(Set<Timeout> set) { + for (; ; ) { + HashedWheelTimeout timeout = pollTimeout(); + if (timeout == null) { + return; + } + if (timeout.isExpired() || timeout.isCancelled()) { + continue; + } + set.add(timeout); + } + } + + private HashedWheelTimeout pollTimeout() { + HashedWheelTimeout head = this.head; + if (head == null) { + return null; + } + HashedWheelTimeout next = head.next; + if (next == null) { + tail = this.head = null; + } else { + this.head = next; + next.prev = null; + } + + // null out prev and next to allow for GC. + head.next = null; + head.prev = null; + head.bucket = null; + return head; + } + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/timer/Timeout.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/timer/Timeout.java new file mode 100644 index 0000000..0dee6e2 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/timer/Timeout.java @@ -0,0 +1,59 @@ +/* + * 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.timer; + +/** + * Derived from Apache Dubbo org.apache.dubbo.common.timer.Timeout + * <a href="https://github.com/apache/dubbo/blob/3.2/dubbo-common/src/main/java/org/apache/dubbo/common/timer/Timeout.java">Timeout.java</a> + * A handle associated with a {@link TimerTask} that is returned by a + * {@link Timer}. + */ +@SuppressWarnings("PMD") +public interface Timeout { + + /** + * Returns the {@link Timer} that created this handle. + */ + Timer timer(); + + /** + * Returns the {@link TimerTask} which is associated with this handle. + */ + TimerTask task(); + + /** + * Returns {@code true} if and only if the {@link TimerTask} associated + * with this handle has been expired. + */ + boolean isExpired(); + + /** + * Returns {@code true} if and only if the {@link TimerTask} associated + * with this handle has been cancelled. + */ + boolean isCancelled(); + + /** + * Attempts to cancel the {@link TimerTask} associated with this handle. + * If the task has been executed or cancelled already, it will return with + * no side effect. + * + * @return True if the cancellation completed successfully, otherwise false + */ + boolean cancel(); +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/timer/Timer.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/timer/Timer.java new file mode 100644 index 0000000..cb50ec7 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/timer/Timer.java @@ -0,0 +1,61 @@ +/* + * 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.timer; + +import java.util.Set; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; + +/** + * Derived from Apache Dubbo org.apache.dubbo.common.timer.Timer + * <a href="https://github.com/apache/dubbo/blob/3.2/dubbo-common/src/main/java/org/apache/dubbo/common/timer/Timer.java">Timer.java</a> + * Schedules {@link TimerTask}s for one-time future execution in a background + * thread. + */ +public interface Timer { + + /** + * Schedules the specified {@link TimerTask} for one-time execution after + * the specified delay. + * + * @param task the {@link TimerTask} + * @param delay the delay + * @param unit the unit of time + * @return a handle which is associated with the specified task + * @throws IllegalStateException if this timer has been {@linkplain #stop() stopped} already + * @throws RejectedExecutionException if the pending timeouts are too many and creating new timeout + * can cause instability in the system. + */ + Timeout newTimeout(TimerTask task, long delay, TimeUnit unit); + + /** + * Releases all resources acquired by this {@link Timer} and cancels all + * tasks which were scheduled but not executed yet. + * + * @return the handles associated with the tasks which were canceled by + * this method + */ + Set<Timeout> stop(); + + /** + * the timer is stop + * + * @return true for stop + */ + boolean isStop(); +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/timer/TimerTask.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/timer/TimerTask.java new file mode 100644 index 0000000..96874fc --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/timer/TimerTask.java @@ -0,0 +1,37 @@ +/* + * 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.timer; + +import java.util.concurrent.TimeUnit; + +/** + * Derived from Apache Dubbo org.apache.dubbo.common.timer.TimerTask + * <a href="https://github.com/apache/dubbo/blob/3.2/dubbo-common/src/main/java/org/apache/dubbo/common/timer/TimerTask.java">TimerTask.java</a> + * A task which is executed after the delay specified with + * {@link Timer#newTimeout(TimerTask, long, TimeUnit)} (TimerTask, long, TimeUnit)}. + */ +public interface TimerTask { + + /** + * Executed after the delay specified with + * {@link Timer#newTimeout(TimerTask, long, TimeUnit)}. + * @param timeout a handle which is associated with this task + * @throws Exception when error happen + */ + void run(Timeout timeout) throws Exception; +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/AesUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/AesUtil.java new file mode 100644 index 0000000..fd1279b --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/AesUtil.java @@ -0,0 +1,165 @@ +/* + * 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.util; + +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import lombok.extern.slf4j.Slf4j; + +/** + * AES Decode Encode Util + */ +@Slf4j +public final class AesUtil { + + /** + * Default encryption key The AES encryption key is 16 bits by default. + * If the AES encryption key is larger than or smaller than 16 bits, an error message is displayed + */ + public static final String DEFAULT_ENCODE_RULES = "tomSun28HaHaHaHa"; + + /** + * Default algorithm + */ + private static final String ALGORITHM_STR = "AES/CBC/PKCS5Padding"; + + private static final String AES = "AES"; + + /** + * Encryption key The AES encryption key is 16 bits. + * If the AES encryption key is larger than 16 bits, an error message is displayed + */ + private static String secretKey = DEFAULT_ENCODE_RULES; + + private AesUtil() {} + + public static void setDefaultSecretKey(String secretKeyNow) { + secretKey = secretKeyNow; + } + + public static String getDefaultSecretKey() { + return secretKey; + } + + public static String aesEncode(String content) { + return aesEncode(content, secretKey); + } + + public static String aesDecode(String content) { + return aesDecode(content, secretKey); + } + + public static boolean isCiphertext(String text) { + return isCiphertext(text, secretKey); + } + + /** + * Encrypted plaintext aes cbc mode + * + * @param content content + * @param encryptKey secretKey + * @return ciphertext + */ + public static String aesEncode(String content, String encryptKey) { + try { + // todo consider not init cipher every time and test performance + SecretKeySpec keySpec = new SecretKeySpec(encryptKey.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.ENCRYPT_MODE, keySpec, new IvParameterSpec(encryptKey.getBytes(StandardCharsets.UTF_8))); + // get content bytes, must utf-8 + byte[] byteEncode = content.getBytes(StandardCharsets.UTF_8); + // encode content to byte array + byte[] byteAes = cipher.doFinal(byteEncode); + // base64 encode content + return Base64.getEncoder().encodeToString(byteAes); + } catch (Exception e) { + log.error("aes encode content error: {}", e.getMessage(), e); + return content; + } + } + + /** + * Decrypt ciphertext + * + * @param content ciphertext + * @param decryptKey secretKey + * @return content + */ + public static String aesDecode(String content, String decryptKey) { + try { + byte[] byteDecode = getBytes(content, decryptKey); + return new String(byteDecode, StandardCharsets.UTF_8); + } catch (BadPaddingException e) { + if (!DEFAULT_ENCODE_RULES.equals(decryptKey)) { + log.warn("There has default encode secret encode content, try to decode with default secret key"); + return aesDecode(content, DEFAULT_ENCODE_RULES); + } + log.error("aes decode content error: {}, please config right common secret key", e.getMessage()); + return content; + } catch (NoSuchAlgorithmException e) { + log.error("no such algorithm: {}", e.getMessage(), e); + } catch (IllegalBlockSizeException e) { + log.error("illegal block size: {}", e.getMessage(), e); + } catch (NullPointerException e) { + log.error("null point exception: {}", e.getMessage(), e); + } catch (Exception e) { + log.error("aes decode error: {}", e.getMessage(), e); + } + return content; + } + + private static byte[] getBytes(final String content, final String decryptKey) throws Exception { + 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); + // decode content to byte array + return cipher.doFinal(bytesContent); + } + + /** + * Determine whether it is encrypted + * @param text text + * @return true false + */ + public static boolean isCiphertext(String text, String decryptKey) { + // First use whether it is base64 to determine whether it has been encrypted + if (Base64Util.isBase64(text)) { + // if it is base64, decrypt directly to determine + try { + byte[] byteDecode = getBytes(text, decryptKey); + return byteDecode != null; + } catch (Exception e) { + log.warn("isCiphertext method error: {}", e.getMessage()); + return false; + } + } + return false; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/ArrowUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/ArrowUtil.java new file mode 100644 index 0000000..54b5143 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/ArrowUtil.java @@ -0,0 +1,171 @@ +/* + * 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.util; + +import lombok.extern.slf4j.Slf4j; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.ipc.ArrowStreamReader; +import org.apache.arrow.vector.ipc.ArrowStreamWriter; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.util.ArrayList; +import java.util.List; + +/** + * Arrow data serialization and deserialization utility class + */ +@Slf4j +public final class ArrowUtil { + + private ArrowUtil() { + } + + /** + * Serialize multiple VectorSchemaRoots into a byte array + * The serialization process: + * 1. Write the number of roots as an integer + * 2. For each root, serialize its content using ArrowStreamWriter + * + * @param roots List of VectorSchemaRoot to be serialized + * @return serialized byte array + */ + public static byte[] serializeMultipleRoots(List<VectorSchemaRoot> roots) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream dataOut = new DataOutputStream(out)) { + + dataOut.writeInt(roots.size()); + for (VectorSchemaRoot root : roots) { + // Use a temporary stream to obtain the precise byte length of a single root, + // write the length, and resolve the pre-read issue. + try (ByteArrayOutputStream tempOut = new ByteArrayOutputStream()) { + try (ArrowStreamWriter writer = new ArrowStreamWriter(root, null, Channels.newChannel(tempOut))) { + writer.start(); + writer.writeBatch(); + writer.end(); + } + int size = tempOut.size(); + dataOut.writeInt(size); + dataOut.flush(); + tempOut.writeTo(out); + } + } + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Failed to serialize multiple VectorSchemaRoots", e); + } + } + + /** + * Deserialize multiple VectorSchemaRoots from a byte array + * The deserialization process: + * 1. Read the number of roots from the first integer + * 2. Create a single RootAllocator for memory management + * 3. Deserialize each root using ArrowStreamReader + * + * @param data byte array containing serialized VectorSchemaRoots + * @return List of deserialized VectorSchemaRoot objects + * @throws RuntimeException if deserialization fails + */ + public static List<VectorSchemaRoot> deserializeMultipleRoots(byte[] data) { + List<VectorSchemaRoot> roots = new ArrayList<>(); + ByteBuffer buffer = ByteBuffer.wrap(data); + try { + int rootCount = buffer.getInt(); + RootAllocator allocator = new RootAllocator(); + + for (int i = 0; i < rootCount; i++) { + int length = buffer.getInt(); + + // Split the InputStream to prevent the Reader from reading beyond its bounds. + ByteArrayInputStream rootIn = new ByteArrayInputStream(data, buffer.position(), length); + buffer.position(buffer.position() + length); + + ArrowStreamReader reader = new ArrowStreamReader( + Channels.newChannel(rootIn), + allocator); + VectorSchemaRoot root = reader.getVectorSchemaRoot(); + reader.loadNextBatch(); + roots.add(root); + } + return roots; + } catch (IOException e) { + roots.forEach(VectorSchemaRoot::close); + throw new RuntimeException("Failed to deserialize multiple VectorSchemaRoots", e); + } + } + + /** + * Deserialize a list of MetricsData from a byte array + * The process: + * 1. Check for null or empty input + * 2. Deserialize VectorSchemaRoots + * 3. Convert each valid root to MetricsData + * 4. Ensure proper resource cleanup + * + * @param data byte array containing serialized metrics data + * @return List of MetricsData objects + */ + public static List<CollectRep.MetricsData> deserializeMetricsData(byte[] data) { + if (data == null || data.length == 0) { + return new ArrayList<>(); + } + List<VectorSchemaRoot> roots = deserializeMultipleRoots(data); + List<CollectRep.MetricsData> metricsDataList = new ArrayList<>(roots.size()); + try { + for (VectorSchemaRoot root : roots) { + if (root != null) { + CollectRep.MetricsData metricsData = new CollectRep.MetricsData(root); + metricsDataList.add(metricsData); + } + } + } finally { + roots.forEach(VectorSchemaRoot::close); + } + return metricsDataList; + } + + /** + * Serialize a list of MetricsData into a byte array + * The process: + * 1. Convert each MetricsData to VectorSchemaRoot + * 2. Serialize all roots into a single byte array + * + * @param metricsDataList List of MetricsData to be serialized + * @return serialized byte array + */ + public static byte[] serializeMetricsData(List<CollectRep.MetricsData> metricsDataList) { + List<VectorSchemaRoot> roots = new ArrayList<>(metricsDataList.size()); + try { + for (CollectRep.MetricsData metricsData : metricsDataList) { + VectorSchemaRoot root = metricsData.toVectorSchemaRootAndRelease(); + roots.add(root); + } + return serializeMultipleRoots(roots); + } finally { + roots.forEach(VectorSchemaRoot::close); + } + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/BackoffUtils.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/BackoffUtils.java new file mode 100644 index 0000000..678b32e --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/BackoffUtils.java @@ -0,0 +1,50 @@ +/* + * 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.util; + +import java.util.concurrent.TimeUnit; + +/** + * Backoff utility class. + */ +public final class BackoffUtils { + + /** Private constructor to prevent instantiation */ + private BackoffUtils() {} + + /** + * Sleeps for the next delay specified by the ExponentialBackoff instance. + * If the thread is interrupted during sleep, it resets the interrupt status + * and returns false to indicate that the operation should not continue. + * + * @param backoff the ExponentialBackoff instance to get the next delay from + * @return true if the sleep completed without interruption, false otherwise + */ + public static boolean shouldContinueAfterBackoff(ExponentialBackoff backoff) { + if (Thread.currentThread().isInterrupted()) { + return false; + } + try { + TimeUnit.MILLISECONDS.sleep(backoff.nextDelay()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/Base64Util.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/Base64Util.java new file mode 100644 index 0000000..ee9962c --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/Base64Util.java @@ -0,0 +1,47 @@ +/* + * 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.util; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * base64 util + */ +public final class Base64Util { + + private Base64Util() { + } + + public static boolean isBase64(String base64) { + + if (base64 == null || base64.isEmpty()) { + return false; + } + + try { + return Base64.getDecoder().decode(base64) != null; + } catch (Exception e) { + return false; + } + } + + public static String encode(String str) { + return Base64.getEncoder().encodeToString(str.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/CommonUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/CommonUtil.java new file mode 100644 index 0000000..35d6a57 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/CommonUtil.java @@ -0,0 +1,263 @@ +/* + * 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.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalTime; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +/** + * common util + */ +@Slf4j +public final class CommonUtil { + + private static final Pattern EMAIL_PATTERN = Pattern.compile("\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*"); + + private static final Pattern PHONE_PATTERN = Pattern.compile("^(((13[0-9])|(14[0-9])|(15[0-9])|(16[0-9])|(19[0-9])|(18[0-9])|(17[0-9]))+\\d{8})?$"); + + private static final Pattern NUMBER_PATTERN = Pattern.compile("^[-+]?[0-9]*\\.?[0-9]+$"); + + private static final int PHONE_LENGTH = 11; + + private CommonUtil() { + } + + /** + * Converts the string str to the int numeric type + * + * @param str string + * @return double number + */ + public static Integer parseStrInteger(final String str) { + if (StringUtils.isBlank(str)) { + return null; + } + + try { + return Integer.parseInt(str); + } catch (Exception e) { + log.debug(e.getMessage(), e); + return null; + } + } + + /** + * Converts the string str to the double number type + * + * @param str string + * @return double number + */ + public static Double parseStrDouble(final String str) { + if (StringUtils.isBlank(str)) { + return null; + } + + try { + return Double.parseDouble(str); + } catch (Exception e) { + log.debug(e.getMessage(), e); + return null; + } + } + + /** + * whether the string is numeric + * @param str string + * @return boolean + */ + public static boolean isNumeric(String str) { + if (StringUtils.isBlank(str)) { + return false; + } + return NUMBER_PATTERN.matcher(str).matches(); + } + + /** + * Converts the time string str to seconds + * + * @param str string + * @return double number + */ + public static int parseTimeStrToSecond(final String str) { + if (StringUtils.isEmpty(str)) { + return -1; + } + + try { + return LocalTime.parse(str).toSecondOfDay(); + } catch (Exception e) { + log.debug(e.getMessage(), e); + return -1; + } + } + + /** + * Converts the string str, which may contain units, to the double number type + * Limit numeric values to four decimal places + * + * @param str string + * @param unit STRING UNIT + * @return DOUBLE DIGITS IN STRING FORMAT Decimal point up to 4 places + */ + public static String parseDoubleStr(String str, String unit) { + if (StringUtils.isBlank(str)) { + return null; + } + + try { + if (unit != null && str.endsWith(unit)) { + str = str.substring(0, str.length() - unit.length()); + } + BigDecimal bigDecimal = new BigDecimal(str); + return bigDecimal.setScale(4, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString(); + } catch (Exception e) { + log.debug(e.getMessage(), e); + return null; + } + } + + /** + * Mailbox format check + * + * @param email email + * @return Is the verification successful + */ + public static boolean validateEmail(final String email) { + if (StringUtils.isBlank(email)) { + return false; + } + + Matcher m = EMAIL_PATTERN.matcher(email); + return m.find(); + } + + /** + * Mobile phone format verification + * + * @param phoneNum mobilePhoneNumber + * @return Is the verification successful + */ + public static boolean validatePhoneNum(final String phoneNum) { + if (StringUtils.isBlank(phoneNum) || phoneNum.length() != PHONE_LENGTH) { + return false; + } + + Matcher m = PHONE_PATTERN.matcher(phoneNum); + return m.find(); + } + + public static String getMessageFromThrowable(Throwable throwable) { + if (throwable == null) { + return "throwable is null, unknown error."; + } + String message = null; + Throwable cause = throwable.getCause(); + if (cause != null) { + message = cause.getMessage(); + } + if (message == null || StringUtils.isBlank(message)) { + message = throwable.getMessage(); + } + if (message == null || StringUtils.isBlank(message)) { + message = throwable.getLocalizedMessage(); + } + if (message == null || StringUtils.isBlank(message)) { + message = throwable.toString(); + } + if (message == null || StringUtils.isBlank(message)) { + message = "unknown error."; + } + return message; + } + + public static String removeBlankLine(String value) { + if (value == null) { + return null; + } + return value.replaceAll("(?m)^\\s*$(\\n|\\r\\n)", ""); + } + + public static String getLangMappingValueFromI18nMap(String lang, Map<String, String> i18nMap) { + if (i18nMap == null || i18nMap.isEmpty()) { + return null; + } + return Optional.ofNullable(i18nMap.get(lang)) + .orElse(i18nMap.values().stream() + .filter(Objects::nonNull) + .findFirst().orElse(null)); + } + + public static void validDefineI18n(Map<String, String> i18nMap, String field) { + if (i18nMap == null || i18nMap.isEmpty()) { + return; + } + for (Map.Entry<String, String> entry : i18nMap.entrySet()) { + String value = entry.getValue(); + String lang = entry.getKey(); + if (StringUtils.isBlank(value)) { + throw new IllegalArgumentException("monitoring template " + field + " " + lang + " value can not blank"); + } + } + } + + /** + * generate random word + * @param length length + * @return words + */ + public static String generateRandomWord(int length) { + StringBuilder word = new StringBuilder(); + Random random = new Random(); + // 'a' ASCII + int minVowel = 97; + // 'z' ASCII + int maxVowel = 122; + // 'A' ASCII + int minConsonant = 65; + // 'Z' ASCII + int maxConsonant = 90; + // '0' ASCII + int minDigit = 48; + // '9' ASCII + int maxDigit = 57; + word.append((char) (minVowel + random.nextInt(5))); + for (int i = 1; i < length; i++) { + switch (random.nextInt(3)) { + case 0: + word.append((char) (minVowel + random.nextInt(5))); + break; + case 1: + word.append((char) (minDigit + random.nextInt(10))); + break; + default: + word.append((char) (minConsonant + random.nextInt(26))); + break; + } + } + return word.toString(); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/ExponentialBackoff.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/ExponentialBackoff.java new file mode 100644 index 0000000..0cf2af5 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/ExponentialBackoff.java @@ -0,0 +1,66 @@ +/* + * 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.util; + +/** + * Exponential backoff utility class. + * + * <p>Provides exponentially increasing delays starting from an initial value, + * doubling with each call to {@link #nextDelay()} until reaching the maximum value. + * Call {@link #reset()} to restart from the initial delay.</p> + * + * <p>Note: This class is <b>not</b> thread-safe. Each thread should use its own instance.</p> + */ +public final class ExponentialBackoff { + private final long initial; + private final long max; + private long current; + + public ExponentialBackoff(long initial, long max) { + if (initial <= 0 || max < initial) { + throw new IllegalArgumentException("Invalid exponential backoff params"); + } + this.initial = initial; + this.max = max; + this.current = initial; + } + + /** + * Returns the current delay value and advances to the next delay. + * The delay doubles on each call until it reaches the maximum value. + * + * @return the delay in milliseconds to wait before the next retry + */ + public long nextDelay() { + long delay = this.current; + if (this.current <= this.max / 2) { + this.current = this.current * 2; + } else { + this.current = this.max; + } + return delay; + } + + /** + * Resets the backoff to the initial delay value. + * Should be called when an operation succeeds after previous failures. + */ + public void reset() { + this.current = this.initial; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/HertzBeatKeywordsUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/HertzBeatKeywordsUtil.java new file mode 100644 index 0000000..0549348 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/HertzBeatKeywordsUtil.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.util; + +import org.apache.hertzbeat.common.constants.HertzBeatKeywordsEnum; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Validate JEXL rules + */ +public class HertzBeatKeywordsUtil { + + + private static final Map<String, HertzBeatKeywordsEnum> KEYWORDS = + Arrays.stream(HertzBeatKeywordsEnum.values()) + .collect(Collectors.toMap(HertzBeatKeywordsEnum::getKeyword, e -> e)); + + + /** + * Verify if the field matches any reserved keywords + * + * @param field Field name to verify + */ + public static void verifyKeywords(String field) { + HertzBeatKeywordsEnum keyword = KEYWORDS.get(field); + if (null == keyword) { + return; + } + throw new IllegalArgumentException( + String.format("Field matches keyword `%s`, please set alias `%s`.", + keyword.getKeyword(), keyword.getAlias())); + } +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/IntervalExpressionUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/IntervalExpressionUtil.java new file mode 100644 index 0000000..4fffd21 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/IntervalExpressionUtil.java @@ -0,0 +1,106 @@ +/* + * 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.util; + +import lombok.extern.slf4j.Slf4j; + +/** + * expression calculate for number range + * [a,b] = {a <= x <= b} + * [a,b) = {a <= x < b} + * [a,+∞) = {a <= x} + * (-∞,b] = {x <= b} + * (-∞,a]||[b,+∞) = {x <= a || x >= b} + */ +@Slf4j +public final class IntervalExpressionUtil { + + private static final String SPLIT_OR = "\\|\\|"; + private static final String SPLIT_AND = ","; + private static final String SPLIT_EQU_LEFT = "("; + private static final String SPLIT_EQU_RIGHT = ")"; + private static final String SPLIT_EQ_LEFT = "["; + private static final String SPLIT_EQ_RIGHT = "]"; + private static final String NEGATIVE = "-∞"; + private static final String POSITIVE = "+∞"; + + private IntervalExpressionUtil() { + } + + /** + * CHECK WHETHER THE VALUE IS IN AN INTERVAL RANGE + * @param numberValue NumericalValue + * @param expression INTERVAL EXPRESSION + * @return true-yes false-no + */ + public static boolean validNumberIntervalExpress(Double numberValue, String expression) { + if (expression == null || expression.isEmpty()) { + return true; + } + if (numberValue == null) { + return false; + } + try { + String[] expressions = expression.split(SPLIT_OR); + for (String expr : expressions) { + String[] values = expr.substring(1, expr.length() - 1).split(SPLIT_AND); + if (values.length != 2) { + continue; + } + Double[] doubleValues = new Double[2]; + if (NEGATIVE.equals(values[0])) { + doubleValues[0] = Double.NEGATIVE_INFINITY; + } else { + doubleValues[0] = Double.parseDouble(values[0]); + } + if (POSITIVE.equals(values[1])) { + doubleValues[1] = Double.POSITIVE_INFINITY; + } else { + doubleValues[1] = Double.parseDouble(values[1]); + } + String startBracket = expr.substring(0, 1); + String endBracket = expr.substring(expr.length() - 1); + if (SPLIT_EQU_LEFT.equals(startBracket)) { + if (SPLIT_EQU_RIGHT.equals(endBracket)) { + if (numberValue > doubleValues[0] && numberValue < doubleValues[1]) { + return true; + } + } else if (SPLIT_EQ_RIGHT.equals(endBracket)) { + if (numberValue > doubleValues[0] && numberValue <= doubleValues[1]) { + return true; + } + } + } else if (SPLIT_EQ_LEFT.equals(startBracket)) { + if (SPLIT_EQU_RIGHT.equals(endBracket)) { + if (numberValue >= doubleValues[0] && numberValue < doubleValues[1]) { + return true; + } + } else if (SPLIT_EQ_RIGHT.equals(endBracket)) { + if (numberValue >= doubleValues[0] && numberValue <= doubleValues[1]) { + return true; + } + } + } + } + return false; + } catch (Exception e) { + log.debug(e.getMessage(), e); + return false; + } + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/IpDomainUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/IpDomainUtil.java new file mode 100644 index 0000000..83eabdf --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/IpDomainUtil.java @@ -0,0 +1,170 @@ +/* + * 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.util; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.UnknownHostException; +import java.util.Enumeration; +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.http.conn.util.InetAddressUtils; + +/** + * ipv4 ipv6 domain util. + */ +@Slf4j +public final class IpDomainUtil { + + private static final Pattern DOMAIN_PATTERN = + Pattern.compile("^[-\\w]+(\\.[-\\w]+)*$"); + + private static final String LOCALHOST = "localhost"; + + /** + * HTTP header schema. + */ + private static final Pattern DOMAIN_SCHEMA = Pattern.compile("^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://){1}[^\\s]*"); + + private IpDomainUtil() { + } + + /** + * whether it is ip or domain. + * @param ipDomain ip domain string + * @return true-yes false-no + */ + public static boolean validateIpDomain(String ipDomain) { + if (ipDomain == null || ipDomain.trim().isEmpty()) { + return false; + } + ipDomain = ipDomain.trim(); + if (LOCALHOST.equalsIgnoreCase(ipDomain)) { + return true; + } + if (InetAddressUtils.isIPv4Address(ipDomain)) { + return true; + } + if (InetAddressUtils.isIPv6Address(ipDomain)) { + return true; + } + return DOMAIN_PATTERN.matcher(ipDomain).matches(); + } + + /** + * if domain or ip has http / https schema. + * @param domainIp host + * @return true or false + */ + public static boolean isHasSchema(String domainIp) { + if (domainIp == null || domainIp.trim().isEmpty()) { + return false; + } + return DOMAIN_SCHEMA.matcher(domainIp).matches(); + } + + /** + * if instance has the port with mark + * @param instance instance ip:port + * @return true if has + */ + public static boolean isHasPortWithMark(String instance) { + if (instance == null || instance.trim().isEmpty()) { + return false; + } + String[] parts = instance.split(SignConstants.DOUBLE_MARK); + if (parts.length >= 2) { + String port = parts[parts.length - 1]; + return CommonUtil.isNumeric(port); + } + return false; + } + + /** + * get localhost IP. + * @return ip + */ + public static String getLocalhostIp() { + try { + Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces(); + InetAddress ip; + while (allNetInterfaces.hasMoreElements()) { + NetworkInterface netInterface = allNetInterfaces.nextElement(); + if (!netInterface.isLoopback() && !netInterface.isVirtual() && netInterface.isUp()) { + Enumeration<InetAddress> addresses = netInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + ip = addresses.nextElement(); + if (ip instanceof Inet4Address) { + return ip.getHostAddress(); + } + } + } + } + } catch (Exception e) { + log.warn(e.getMessage()); + } + return null; + } + + /** + * check IP address type. + * @param ipDomain ip domain + * @return IP address type + */ + public static String checkIpAddressType(String ipDomain){ + if (ipDomain != null && !ipDomain.trim().isEmpty() && InetAddressUtils.isIPv6Address(ipDomain)) { + return NetworkConstants.IPV6; + } + return NetworkConstants.IPV4; + } + + /** + * get current local host name. + * @return hostname + */ + public static String getCurrentHostName() { + try { + InetAddress inetAddress = InetAddress.getLocalHost(); + return inetAddress.getHostName(); + } catch (UnknownHostException e) { + return null; + } + } + + /** + * check port is valid. + * @return true if valid + */ + public static boolean validPort(String portStr) { + + if (portStr == null || portStr.trim().isEmpty()) { + return false; + } + + try { + int port = Integer.parseInt(portStr); + return port >= 0 && port <= 65535; + } catch (NumberFormatException e) { + return false; + } + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/JexlCheckerUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/JexlCheckerUtil.java new file mode 100644 index 0000000..223958c --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/JexlCheckerUtil.java @@ -0,0 +1,50 @@ +/* + * 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.util; + +import org.apache.hertzbeat.common.constants.JexlKeywordsEnum; + +/** + * Validate JEXL rules + */ +public class JexlCheckerUtil { + + public static final String SPACES_REGEX = "^\\S+(?:\\s+\\S+)+$"; + + /** + * Verify Keyword Information + */ + public static boolean verifyKeywords(String str) { + return JexlKeywordsEnum.match(str); + } + + /** + * Verify the starting character + */ + public static boolean verifyStartCharacter(String str) { + char c = str.charAt(0); + return !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '$'); + } + + /** + * Verify contains spaces + */ + public static boolean verifySpaces(String str) { + return str.trim().matches(SPACES_REGEX); + } +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/JexlCommonFunction.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/JexlCommonFunction.java new file mode 100644 index 0000000..bc4b1bc --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/JexlCommonFunction.java @@ -0,0 +1,98 @@ +/* + * 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.util; + +import java.util.regex.Pattern; + +/** + * the common function for jexl str equals, match, contains, etc. + * sys:now() + */ +public class JexlCommonFunction { + + /** + * Get the current time in milliseconds + * @return current time + */ + public long now() { + return System.currentTimeMillis(); + } + + + /** + * Define a custom string equality function + * @param left left + * @param right right + * @return true if equals + */ + public boolean equals(String left, String right) { + if (left == null && right == null) { + return true; + } + if (left == null || right == null) { + return false; + } + return left.equals(right); + } + + /** + * Custom determines whether string 1 contains string 2 (case-insensitive) + * @param left left + * @param right right + * @return true if contains + */ + public boolean contains(String left, String right) { + if (left == null && right == null) { + return true; + } + if (left == null || right == null) { + return false; + } + return left.toLowerCase().contains(right.toLowerCase()); + } + + + /** + * Custom determines if a value exists for this object in the environment + * @param arg arg + * @return true if exists + */ + public boolean exists(Object arg) { + if (arg == null) { + return false; + } + return !String.valueOf(arg).isEmpty(); + } + + /** + * Custom determines if a string matches a regex + * - regex You need to add "" or '' + * @param str str + * @param regex regex + * @return true if matches + */ + public boolean matches(String str, String regex) { + if (str == null || regex == null) { + return false; + } + return Pattern.compile(regex).matcher(str).matches(); + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/JexlExpressionRunner.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/JexlExpressionRunner.java new file mode 100644 index 0000000..b3b6f7c --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/JexlExpressionRunner.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.common.util; + +import com.google.common.collect.Maps; +import org.apache.commons.jexl3.JexlBuilder; +import org.apache.commons.jexl3.JexlContext; +import org.apache.commons.jexl3.JexlEngine; +import org.apache.commons.jexl3.JexlExpression; +import org.apache.commons.jexl3.JexlFeatures; +import org.apache.commons.jexl3.MapContext; +import org.apache.hertzbeat.common.constants.CommonConstants; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.function.Function; +import tools.jackson.core.type.TypeReference; + +/** + * jexl express runner + */ +public class JexlExpressionRunner { + + private static final String LOADER_NAME = "jexl-class-loader"; + private static final JexlEngine jexlEngine; + + static { + Map<String, Object> functions = Maps.newLinkedHashMap(); + // set the root namespace function + functions.put(null, new JexlCommonFunction()); + functions.put(CommonConstants.JEXL_CUSTOM_JSON_FUNCTION, jsonFunction()); + ClassLoader classLoader = new ClassLoader() { + @Override + public String getName() { + return LOADER_NAME; + } + }; + JexlFeatures features = new JexlFeatures(); + features.annotation(false).loops(false).pragma(false) + .methodCall(false).lambda(false).newInstance(false).register(false); + jexlEngine = new JexlBuilder().charset(StandardCharsets.UTF_8).cache(256).loader(classLoader) + .features(features).strict(true).silent(false).stackOverflow(40).namespaces(functions) + .create(); + } + + public static Object evaluate(String expression, Map<String, Object> context) { + JexlContext jexlContext = new MapContext(); + for (Map.Entry<String, Object> entry : context.entrySet()) { + jexlContext.set(entry.getKey(), entry.getValue()); + } + return jexlEngine.createExpression(expression).evaluate(jexlContext); + } + + public static Object evaluate(JexlExpression expression, Map<String, Object> context) { + JexlContext jexlContext = new MapContext(); + for (Map.Entry<String, Object> entry : context.entrySet()) { + jexlContext.set(entry.getKey(), entry.getValue()); + } + return expression.evaluate(jexlContext); + } + + public static Object evaluate(String expression) { + return jexlEngine.createExpression(expression).evaluate(new MapContext()); + } + + public static JexlExpression compile(String expression) { + return jexlEngine.createExpression(expression); + } + + /** + * custom function `json:apply(xx)` + * + * @return java.util.function.Function + */ + private static Function<String, Map<String, Object>> jsonFunction() { + return json -> { + TypeReference<Map<String, Object>> typeReference = new TypeReference<>() { + }; + return JsonUtil.fromJson(json, typeReference); + }; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/JsonUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/JsonUtil.java new file mode 100644 index 0000000..fa0705f --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/JsonUtil.java @@ -0,0 +1,195 @@ +/* + * 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.util; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import java.io.InputStream; +import java.io.OutputStream; +import lombok.extern.slf4j.Slf4j; +import tools.jackson.core.JacksonException; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * json util + */ +@ThreadSafe +@Slf4j +public final class JsonUtil { + + + private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .changeDefaultVisibility(vc -> vc.withVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)) + .build(); + + private JsonUtil() { + } + + public static String toJson(Object source) { + if (source == null) { + return null; + } + try { + return OBJECT_MAPPER.writeValueAsString(source); + } catch (JacksonException e) { + log.error("Error converting object to JSON: {}", e.getMessage(), e); + return null; + } + } + + public static <T> T fromJson(String jsonStr, Class<T> clazz) { + if (jsonStr == null || jsonStr.trim().isEmpty()) { + return null; + } + try { + return OBJECT_MAPPER.readValue(jsonStr, clazz); + } catch (JacksonException e) { + log.error("Error parsing JSON to class {}: {}", clazz.getName(), e.getMessage(), e); + return null; + } + } + + public static <T> T fromJson(String jsonStr, TypeReference<T> type) { + if (jsonStr == null || jsonStr.trim().isEmpty()) { + return null; + } + try { + return OBJECT_MAPPER.readValue(jsonStr, type); + } catch (JacksonException e) { + log.error("Error parsing JSON to TypeReference: {}", e.getMessage(), e); + return null; + } + } + + public static JsonNode fromJson(String jsonStr) { + if (jsonStr == null || jsonStr.trim().isEmpty()) { + return null; + } + try { + return OBJECT_MAPPER.readTree(jsonStr); + } catch (JacksonException e) { + log.error("Error reading JSON tree: {}", e.getMessage(), e); + return null; + } + } + + /** + * check if the string is a json string + * @param jsonStr json string + * @return true if the string is a json string + */ + public static boolean isJsonStr(String jsonStr) { + if (jsonStr == null || jsonStr.trim().isEmpty()) { + return false; + } + jsonStr = jsonStr.trim(); + if (!isJsonLike(jsonStr)) { + return false; + } + try { + OBJECT_MAPPER.readTree(jsonStr); + return true; + } catch (JacksonException ignored) { + return false; + } + } + + public static Boolean isArray(String jsonStr) { + if (!isJsonLike(jsonStr)) { + return false; + } + try { + JsonNode jsonNode = OBJECT_MAPPER.readTree(jsonStr); + return jsonNode.isArray(); + } catch (JacksonException ignore) { + return false; + } + } + + public static boolean isJsonLike(String jsonStr) { + if (jsonStr == null || jsonStr.trim().isEmpty()) { + return false; + } + jsonStr = jsonStr.trim(); + char start = jsonStr.charAt(0); + char end = jsonStr.charAt(jsonStr.length() - 1); + return (start == '{' && end == '}') || (start == '[' && end == ']'); + } + + /** + * Parse JSON from InputStream to object + * @param is input stream + * @param type type reference + * @return parsed object or null if error + */ + public static <T> T fromJson(InputStream is, TypeReference<T> type) { + if (is == null) { + return null; + } + try { + return OBJECT_MAPPER.readValue(is, type); + } catch (JacksonException e) { + log.error("Error parsing JSON from InputStream to TypeReference: {}", e.getMessage(), e); + return null; + } + } + + /** + * Write object to OutputStream as JSON + * @param source object to serialize + * @param os output stream + */ + public static void toJson(Object source, OutputStream os) { + if (source == null || os == null) { + return; + } + try { + OBJECT_MAPPER.writeValue(os, source); + } catch (JacksonException e) { + log.error("Error writing object to OutputStream as JSON: {}", e.getMessage(), e); + } + } + + /** + * Convert a value from one type to another using JSON serialization/deserialization + * @param fromValue source value + * @param toValueType target type + * @return converted value or null if error + */ + public static <T> T convertValue(Object fromValue, Class<T> toValueType) { + if (fromValue == null) { + return null; + } + try { + return OBJECT_MAPPER.convertValue(fromValue, toValueType); + } catch (JacksonException e) { + log.error("Error converting value to {}: {}", toValueType.getName(), e.getMessage(), e); + return null; + } + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/LogUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/LogUtil.java new file mode 100644 index 0000000..a325580 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/LogUtil.java @@ -0,0 +1,167 @@ +/* + * 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.util; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.slf4j.Logger; + +import java.text.MessageFormat; + +/** + * Log utility class that provides formatted logging methods with location information. + * This class enhances standard SLF4J logging by automatically adding caller location details. + */ +public class LogUtil { + + private static final String TEMPLATE_REGEX = "\\{\\d}"; + + /** + * Print debug level formatted log + * Example: LogUtil.debug(logger, "hello,{0},here has a {1} exception", "other information"); + */ + @SuppressWarnings("unused") + public static void debug(Logger logger, String msg, Object... params) { + if (logger.isDebugEnabled()) { + + if (ArrayUtils.isEmpty(params)) { + logger.debug(LogUtil.buildLocationInfo() + msg); + } else { + logger.debug(LogUtil.buildLocationInfo() + format(msg, params)); + } + } + } + + + /** + * Print info level formatted log + * Example: LogUtil.info(logger, "hello,{0},{1} exception", "dear", "database operation"); + */ + public static void info(Logger logger, String msg, Object... params) { + if (logger.isInfoEnabled()) { + if (ArrayUtils.isEmpty(params)) { + logger.info(LogUtil.buildLocationInfo() + msg); + } else { + logger.info(LogUtil.buildLocationInfo() + format(msg, params)); + } + } + } + + /** + * Print warn level formatted log + */ + public static void warn(Logger logger, String msg, Object... params) { + if (logger.isWarnEnabled()) { + if (ArrayUtils.isEmpty(params)) { + logger.warn(LogUtil.buildLocationInfo() + msg); + } else { + logger.warn(LogUtil.buildLocationInfo() + format(msg, params)); + } + } + } + + /** + * Print error level formatted log, use {0},{1},.. for parameter replacement + * Example: LogUtil.error(logger, "hello,{0}, a {1} exception occurred here", "dear", "database operation"); + */ + public static void error(Logger logger, String msg, Object... params) { + if (logger.isErrorEnabled()) { + if (ArrayUtils.isEmpty(params)) { + logger.error(LogUtil.buildLocationInfo() + msg); + } else { + logger.error(LogUtil.buildLocationInfo() + format(msg, params)); + } + } + + } + + + /** + * Print warn level formatted log with exception, use {0},{1},.. for parameter replacement + * Example: LogUtil.warn(logger, e, "hello,{0}, a {1} exception occurred here", "dear", "database operation"); + */ + public static void warn(Logger logger, Throwable e, String msg, Object... params) { + if (logger.isWarnEnabled()) { + if (ArrayUtils.isEmpty(params)) { + logger.warn(LogUtil.buildLocationInfo() + msg, e); + } else { + logger.warn(LogUtil.buildLocationInfo() + format(msg, params), e); + } + } + } + + + /** + * Print error level formatted log with exception, use {0},{1},.. for parameter replacement + * Example: LogUtil.error(logger, e, "hello,{0}, a {1} exception occurred here", "dear", "database operation"); + */ + public static void error(Logger logger, Throwable e, String msg, Object... params) { + if (logger.isErrorEnabled()) { + if (ArrayUtils.isEmpty(params)) { + logger.error(LogUtil.buildLocationInfo() + msg, e); + } else { + logger.error(LogUtil.buildLocationInfo() + format(msg, params), e); + } + } + + } + + + /** + * Get the class name, method and line number that calls LogUtil + * + * @return location information string + */ + private static String buildLocationInfo() { + StringBuilder header = new StringBuilder(); + // LOG4J2-1029 new Throwable().getStackTrace is faster than Thread.currentThread().getStackTrace(). + final StackTraceElement[] stackTraceElements = new Throwable().getStackTrace(); + + for (int i = 0; i < stackTraceElements.length - 1; i++) { + StackTraceElement currentStackTrace = stackTraceElements[i]; + StackTraceElement nextStackTrace = stackTraceElements[i + 1]; + + // If current stack trace is in LogUtil + // and next stack trace is not in LogUtil + // then the next node is the caller of LogUtil + if (LogUtil.class.getName().equals(currentStackTrace.getClassName()) + && !LogUtil.class.getName().equals(nextStackTrace.getClassName())) { + String stackTrace = nextStackTrace.toString(); + header.append(" ").append(StringUtils.removeStart(stackTrace, nextStackTrace.getClassName() + ".")); + break; + } + } + return header.append(":").toString(); + } + + private static String format(String msg, Object... params) { + if (StringUtils.isEmpty(msg)) { + return StringUtils.EMPTY; + } + if (params != null && params.length > 0) { + msg = MessageFormat.format(msg, params); + } + return msg.replaceAll(TEMPLATE_REGEX, StringUtils.EMPTY); + } + + private static String toString(Object object) { + return ToStringBuilder.reflectionToString(object, ToStringStyle.SHORT_PREFIX_STYLE); + } +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/LruHashMap.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/LruHashMap.java new file mode 100644 index 0000000..c88f40a --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/LruHashMap.java @@ -0,0 +1,41 @@ +/* + * 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.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * lru hash map + * The least recently used elimination algorithm map + */ +public final class LruHashMap<K, V> extends LinkedHashMap<K, V> { + + private final int threshold; + + public LruHashMap(int threshold) { + super(16, 0.75f, true); + this.threshold = threshold; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > threshold; + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/MapCapUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/MapCapUtil.java new file mode 100644 index 0000000..444e706 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/MapCapUtil.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.util; + +/** + * map initial capacity calculation + */ +public final class MapCapUtil { + + private MapCapUtil() { + } + + private static final float LOAD_FACTOR = 0.75f; + + /** + * Prevent expansion + * @param size size + * @return capacity + */ + public static int calInitMap(int size) { + return (int) Math.ceil (size / LOAD_FACTOR); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/NetworkUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/NetworkUtil.java new file mode 100644 index 0000000..e1f7361 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/NetworkUtil.java @@ -0,0 +1,62 @@ +/* + * 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.util; + +/** + * network util + */ +public final class NetworkUtil { + + public static final String OS_NAME = System.getProperty("os.name"); + + private static final String LINUX = "linux"; + + private static final String WINDOWS = "windows"; + + private static boolean isLinuxPlatform = false; + private static boolean isWindowsPlatform = false; + + static { + if (OS_NAME != null && OS_NAME.toLowerCase().contains(LINUX)) { + isLinuxPlatform = true; + } + + if (OS_NAME != null && OS_NAME.toLowerCase().contains(WINDOWS)) { + isWindowsPlatform = true; + } + } + + private NetworkUtil() { + } + + /** + * whether the running environment is linux + * @return is linux platform or not + */ + public static boolean isLinuxPlatform() { + return isLinuxPlatform; + } + + /** + * whether the running environment is windows + * @return is windows platform or not + */ + public static boolean isWindowsPlatform() { + return isWindowsPlatform; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/Pair.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/Pair.java new file mode 100644 index 0000000..6f3cc78 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/Pair.java @@ -0,0 +1,47 @@ +/* + * 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.util; + +/** + * Pair KV + */ +public final class Pair<K, V> { + private K left; + private final V right; + + public Pair(K left, V right) { + this.left = left; + this.right = right; + } + + public static <K, V> Pair<K, V> of(K left, V right) { + return new Pair<>(left, right); + } + + public K getLeft() { + return left; + } + + public void setLeft(K left) { + this.left = left; + } + + public V getRight() { + return right; + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/ProtoJsonUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/ProtoJsonUtil.java new file mode 100644 index 0000000..e3d68b6 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/ProtoJsonUtil.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.util; + +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; + +/** + * protobuf json convert util + */ +@Slf4j +public final class ProtoJsonUtil { + + private static final JsonFormat.Printer PRINTER = JsonFormat.printer(); + private static final JsonFormat.Parser PARSER = JsonFormat.parser(); + + private ProtoJsonUtil() { + } + + /** + * protobuf to json + * @param proto protobuf + * @return json + */ + public static String toJsonStr(Message proto) { + + if (Objects.isNull(proto)) { + log.error("proto is null"); + return null; + } + + try { + return PRINTER.print(proto); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + /** + * json to protobuf + * @param json json str + * @param builder proto instance builder + * @return protobuf + */ + public static Message toProtobuf(String json, Message.Builder builder) { + + if (Objects.isNull(json) || Objects.isNull(builder)) { + log.error("json or builder is null"); + return null; + } + + try { + PARSER.merge(json, builder); + return builder.build(); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/ResourceBundleUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/ResourceBundleUtil.java new file mode 100644 index 0000000..28a0caa --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/ResourceBundleUtil.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.util; + +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.support.ResourceBundleUtf8Control; + +/** + * i18n ResourceBundle util + */ +@Slf4j +public final class ResourceBundleUtil { + + private static final ResourceBundleUtf8Control BUNDLE_UTF_8_CONTROL = new ResourceBundleUtf8Control(); + private static final Integer LANG_REGION_LENGTH = 2; + + static { + // set default locale by env + try { + String langEnv = System.getenv("LANG"); + if (langEnv != null) { + String[] langArr = langEnv.split("\\."); + if (langArr.length >= 1) { + String[] regionArr = langArr[0].split("_"); + if (regionArr.length == LANG_REGION_LENGTH) { + String language = regionArr[0]; + String region = regionArr[1]; + Locale locale = new Locale(language, region); + Locale.setDefault(locale); + } + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + private ResourceBundleUtil() { + } + + /** + * get resource bundle by bundle name + * @param bundleName bundle name + * @return resource bundle + */ + public static ResourceBundle getBundle(String bundleName) { + try { + return ResourceBundle.getBundle(bundleName, BUNDLE_UTF_8_CONTROL); + } catch (MissingResourceException resourceException) { + return ResourceBundle.getBundle(bundleName, Locale.US, BUNDLE_UTF_8_CONTROL); + } + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/SnowFlakeIdGenerator.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/SnowFlakeIdGenerator.java new file mode 100644 index 0000000..f6f287e --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/SnowFlakeIdGenerator.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.util; + +/** + * Snowflake Algorithm Generator Tool + */ +public class SnowFlakeIdGenerator { + + private static final SnowFlakeIdWorker ID_WORKER; + + static { + ID_WORKER = new SnowFlakeIdWorker(); + } + + public static long generateId() { + return ID_WORKER.nextId(); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/SnowFlakeIdWorker.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/SnowFlakeIdWorker.java new file mode 100644 index 0000000..f111069 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/SnowFlakeIdWorker.java @@ -0,0 +1,155 @@ +/* + * 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.util; + +import java.util.Random; +import lombok.extern.slf4j.Slf4j; + +/** + * SnowFlakeId Instance + * Note that due to the front-end JS TS in json parsing large numbers will cause loss of precision + * UUID cannot exceed 9007199254740991 (10) Hexadecimal 0x1FFFFFFFFFFFFFFFFF (less than 53bit) + * 1 bit symbol +41 bit timestamp +4 bit machine ID+8 bit sequence number = 53 bits + */ +@Slf4j +public class SnowFlakeIdWorker { + + /** + * Start timestamp, in milliseconds; This is 2021-06-01 + */ + private static final long TW_EPOCH = 1622476800000L; + + /** + * The number of bits occupied by the machine ID + */ + private static final long WORKER_ID_BITS = 4L; + + /** + * Maximum machine ID supported, 0-15 + * <p> + * The source code of PS.Twitter is -1L ^ (-1L << workerIdBits); Here the final xor operation with -1, + * because of the particularity of -1's binary complement, it is equivalent to taking the inverse. + */ + private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); + + /** + * The number of bits the sequence occupies in the ID + */ + private static final long SEQUENCE_BITS = 8L; + + /** + * Number of machine ID shifts left + */ + private static final long WORKER_ID_SHIFT = SEQUENCE_BITS; + + /** + * Time truncated left shift number + */ + private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; + + /** + * The maximum mask of the generated sequence, 256 + */ + private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); + + /** + * Working machine ID(0~15) + */ + private final long workerId; + + /** + * Millisecond sequence (0~256) + */ + private long sequence = 0L; + + /** + * Timestamp of the last ID generated + */ + private long lastTimestamp = -1L; + + /** + * How to create an ID generator: Use the serial number range of the working machine [0, 15] + * + * @param workerId Working machine ID + */ + public SnowFlakeIdWorker(long workerId) { + if (workerId < 0 || workerId > MAX_WORKER_ID) { + Random random = new Random(workerId); + workerId = random.nextInt((int) MAX_WORKER_ID); + log.warn("Worker ID can't be greater than {} or less than 0, use random: {}.", MAX_WORKER_ID, workerId); + } + this.workerId = workerId; + } + + /** + * How to create an ID generator: Create the generator using the local IP as the machine ID + */ + public SnowFlakeIdWorker() { + int workerId = 0; + String host = IpDomainUtil.getLocalhostIp(); + if (host == null) { + Random random = new Random(workerId); + workerId = random.nextInt((int) MAX_WORKER_ID); + } else { + workerId = host.hashCode() % (int) MAX_WORKER_ID; + workerId = Math.abs(workerId); + } + this.workerId = workerId; + } + + /** + * get next id + * thread safe + * @return id with 15 length + */ + public synchronized long nextId() { + long timestamp = timeGen(); + if (lastTimestamp == timestamp) { + // Generated at the same time, then the sequence number +1 + sequence = (sequence + 1) & SEQUENCE_MASK; + // Sequence overflow in milliseconds: The maximum value is exceeded + if (sequence == 0) { + // Block to the next millisecond to get a new timestamp + timestamp = tilNextMillis(lastTimestamp); + } + } else { + // The timestamp changes and the sequence resets in milliseconds + sequence = 0L; + } + // Timestamp of the last ID generated + lastTimestamp = timestamp; + + // Shift it and put it together with the OR operation + return ((timestamp - TW_EPOCH) << TIMESTAMP_LEFT_SHIFT) + | (workerId << WORKER_ID_SHIFT) + | sequence; + } + + private long tilNextMillis(long lastTimestamp) { + long timestamp = timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = timeGen(); + } + return timestamp; + } + + private long timeGen() { + return System.currentTimeMillis(); + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/StrBuffer.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/StrBuffer.java new file mode 100644 index 0000000..c15bc33 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/StrBuffer.java @@ -0,0 +1,166 @@ +/* + * 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.util; + +/** + * In order to avoid generating too many String objects in the way of subString during ExporterParser parsing, + * use this class control + */ +public class StrBuffer { + private static final String POSITIVE_INF = "+inf"; + private static final String NEGATIVE_INF = "-inf"; + private static final long POSITIVE_INF_VALUE = 0x7FF0000000000000L; + private static final long NEGATIVE_INF_VALUE = 0xFFF0000000000000L; + + private final char[] chars; + private int left; + private int right; + + public StrBuffer(String s) { + this.chars = s.toCharArray(); + this.left = 0; + this.right = s.length() - 1; + } + + /** + * Reading the current character, left++ + * + * @return Current subscript character + */ + public char read() { + if (left > right) { + throw new IndexOutOfBoundsException("StrBuffer use charAt method error. left=" + left + ", right=" + right); + } + return chars[left++]; + } + + /** + * Rollback one character + */ + public void rollback() { + if (left > 0) { + left--; + } + } + + /** + * Only the index character of left+i is queried; there is no left++ operation + * + * @param i index + * @return left+iThe character corresponding to the index + */ + public char charAt(int i) { + if (left + i > right) { + throw new IndexOutOfBoundsException("StrBuffer use charAt method error. left=" + left + ", i=" + i); + } + return chars[left + i]; + } + + /** + * Converting a string object + * + * @return charA string corresponding to an array + */ + public String toStr() { + StringBuilder builder = new StringBuilder(); + for (int i = left; i <= right; i++) { + builder.append(chars[i]); + } + return builder.toString(); + } + + /** + * transition double + * + * @return char double integer corresponding to the array + */ + public double toDouble() { + String s = toStr(); + return parseDouble(s); + } + + /** + * transition long + * + * @return char the long integer corresponding to the array + */ + public long toLong() { + String s = toStr(); + return parseLong(s); + } + + public void skipBlankTabs() { + while (left <= right) { + if (this.isBlankOrTab(chars[left])) { + left++; + } else { + break; + } + } + while (right >= left) { + if (this.isBlankOrTab(chars[right])) { + right--; + } else { + break; + } + } + } + + private boolean isBlankOrTab(char c) { + return c == ' ' || c == '\t'; + } + + public boolean isEmpty() { + return left > right; + } + + /** + * string -> long, We need to determine if it's INF + * + * @param s string + * @return long + */ + public static long parseLong(String s) { + if (POSITIVE_INF.equalsIgnoreCase(s)) { + return POSITIVE_INF_VALUE; + } + if (NEGATIVE_INF.equalsIgnoreCase(s)) { + return NEGATIVE_INF_VALUE; + } + return Double.valueOf(s).longValue(); + } + + /** + * string -> double, We need to determine if it's INF + * + * @param s string + * @return double + */ + public static double parseDouble(String s) { + if (POSITIVE_INF.equalsIgnoreCase(s)) { + return POSITIVE_INF_VALUE; + } else if (NEGATIVE_INF.equalsIgnoreCase(s)) { + return NEGATIVE_INF_VALUE; + } + return Double.parseDouble(s); + } + + public static String escapeForFormat(String value) { + return value.replace("%", "%%"); + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/StrUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/StrUtil.java new file mode 100644 index 0000000..b6aaa53 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/StrUtil.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.util; + + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * String processing tools + */ +public final class StrUtil { + + private StrUtil() { + } + + /** + * Handle Comma Separated Data + * @param arg Comma separated data + * @return Data List + */ + public static List<String> analysisArgToList(String arg) { + if (Objects.isNull(arg)) { + return Collections.emptyList(); + } + return Arrays.asList(arg.split("\\s*,\\s*")); + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/TimePeriodUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/TimePeriodUtil.java new file mode 100644 index 0000000..de342f8 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/TimePeriodUtil.java @@ -0,0 +1,94 @@ +/* + * 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.util; + +import java.time.Duration; +import java.time.Period; +import java.time.temporal.TemporalAmount; +import lombok.extern.slf4j.Slf4j; + +/** + * time util + */ +@Slf4j +public final class TimePeriodUtil { + + private TimePeriodUtil() { + } + + /** + * parse tokenTime to TemporalAmount + * @param tokenTime eg: "1m", "5M", "3D", "30m", "2h", "1Y", "3W" + * @return TemporalAmount + */ + public static TemporalAmount parseTokenTime(String tokenTime) { + + if (tokenTime == null || tokenTime.length() < 2) { + log.error("tokenTime is invalid"); + return null; + } + + if (Character.isUpperCase(tokenTime.charAt(tokenTime.length() - 1))) { + return Period.parse("P" + tokenTime); + } else { + return Duration.parse("PT" + tokenTime); + } + } + + /** + * transform any timestamp to milliseconds + * @param timestamp timestamp + * @return milliseconds + */ + public static long normalizeToMilliseconds(Object timestamp) { + if (timestamp instanceof String timestampStr) { + // string type, may be second, millisecond or decimal second + // eg: "1672531199000", "1672531199", "1672531199.123" + if (timestampStr.contains(".")) { + // contains decimal point, parse as second timestamp + double seconds = Double.parseDouble(timestampStr); + return (long) (seconds * 1000); + } else { + // integer form, determine second or millisecond + long numericTimestamp = Long.parseLong(timestampStr); + return convertNumericTimestamp(numericTimestamp); + } + } else if (timestamp instanceof Number) { + // number eg Integer、Long、Double + if (timestamp instanceof Double || timestamp instanceof Float) { + // float type, treat as second timestamp + double seconds = ((Number) timestamp).doubleValue(); + return (long) (seconds * 1000); + } else { + // integer type, directly determine second or millisecond + long numericTimestamp = ((Number) timestamp).longValue(); + return convertNumericTimestamp(numericTimestamp); + } + } else { + throw new IllegalArgumentException("Not support this timestamp type: " + timestamp.getClass().getName()); + } + } + + private static long convertNumericTimestamp(long numericTimestamp) { + if (String.valueOf(numericTimestamp).length() <= 10) { + return numericTimestamp * 1000; + } else { + return numericTimestamp; + } + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/TimeZoneUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/TimeZoneUtil.java new file mode 100644 index 0000000..5772581 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/TimeZoneUtil.java @@ -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.common.util; + +import java.util.Locale; +import java.util.TimeZone; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.constants.CommonConstants; + +/** + * timezone util + */ +public final class TimeZoneUtil { + + private TimeZoneUtil() { + } + + private static final Integer LANG_REGION_LENGTH = 2; + + public static void setTimeZoneAndLocale(String timeZoneId, String locale) { + setTimeZone(timeZoneId); + setLocale(locale); + } + + public static void setTimeZone(String timeZoneId) { + if (StringUtils.isBlank(timeZoneId)) { + return; + } + + TimeZone.setDefault(TimeZone.getTimeZone(timeZoneId)); + } + + public static void setLocale(String locale) { + if (StringUtils.isBlank(locale)) { + return; + } + + String[] arr = locale.split(CommonConstants.LOCALE_SEPARATOR); + if (arr.length == LANG_REGION_LENGTH) { + String language = arr[0]; + String country = arr[1]; + Locale.setDefault(new Locale(language, country)); + } + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/XmlUtil.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/XmlUtil.java new file mode 100644 index 0000000..8c34040 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/XmlUtil.java @@ -0,0 +1,77 @@ +/* + * 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.util; + + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import tools.jackson.core.JacksonException; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.dataformat.xml.XmlMapper; + +/** + * xml util + */ +@Slf4j +public class XmlUtil { + + private static final XmlMapper XML_MAPPER = XmlMapper.builder() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .build(); + + private XmlUtil() { + } + + /** + * Convert object to XML string + */ + public static String toXml(Object source) { + if (source == null) { + return null; + } + return XML_MAPPER.writeValueAsString(source); + } + + /** + * Convert XML string to object + */ + public static <T> T fromXml(String xml, Class<T> clazz) { + if (StringUtils.isEmpty(xml)) { + return null; + } + return XML_MAPPER.readValue(xml, clazz); + } + + /** + * Convert XML string to object with TypeReference + */ + public static <T> T fromXml(String xml, TypeReference<T> type) { + if (!StringUtils.isEmpty(xml)) { + return null; + } + try { + return XML_MAPPER.readValue(xml, type); + } catch (JacksonException e) { + log.error("Error parsing XML to TypeReference: {}", e.getMessage(), e); + return null; + } + } +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/export/ExcelExportUtils.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/export/ExcelExportUtils.java new file mode 100644 index 0000000..f26a712 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/export/ExcelExportUtils.java @@ -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.common.util.export; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; + +/** + * Excel export utils + */ + +public final class ExcelExportUtils { + + private ExcelExportUtils() { + } + + /** + * set cell style + * @param workbook workbook entity + */ + public static CellStyle setCellStyle(Workbook workbook) { + + CellStyle cellStyle = workbook.createCellStyle(); + cellStyle.setAlignment(HorizontalAlignment.CENTER); + return cellStyle; + } + + /** + * @param clazz Export entity class + */ + public static <T extends Class<?>> Sheet setSheet(String sheetName, Workbook workbook, T clazz) { + + var sheet = workbook.createSheet(sheetName); + sheet.setDefaultColumnWidth(20); + sheet.setColumnWidth(9, 40 * 256); + sheet.setColumnWidth(10, 40 * 256); + + // set header style + CellStyle headerCellStyle = workbook.createCellStyle(); + Font headerFont = workbook.createFont(); + headerFont.setBold(true); + headerCellStyle.setFont(headerFont); + headerCellStyle.setAlignment(HorizontalAlignment.CENTER); + + List<String> headers = new ArrayList<>(); + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + headers.add(field.getName()); + } + + // set header + Row headerRow = sheet.createRow(0); + String[] headerArray = headers.toArray(new String[0]); + for (int i = 0; i < headerArray.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headerArray[i]); + cell.setCellStyle(headerCellStyle); + } + + return sheet; + } + +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/export/YamlExportUtils.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/export/YamlExportUtils.java new file mode 100644 index 0000000..a084d72 --- /dev/null +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/export/YamlExportUtils.java @@ -0,0 +1,44 @@ +/* + * 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.util.export; + +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +/** + * Yaml Export Utils + */ + +public final class YamlExportUtils { + + private YamlExportUtils() { + } + + public static <T> void exportWriteOs(List<T> list, OutputStream os) { + + var options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setIndent(2); + options.setPrettyFlow(true); + Yaml yaml = new Yaml(options); + yaml.dump(list, new OutputStreamWriter(os, StandardCharsets.UTF_8)); + } + +} diff --git a/hertzbeat-common-core/src/main/message/cluster_msg.proto b/hertzbeat-common-core/src/main/message/cluster_msg.proto new file mode 100644 index 0000000..bcc1335 --- /dev/null +++ b/hertzbeat-common-core/src/main/message/cluster_msg.proto @@ -0,0 +1,62 @@ +/* + * 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. + */ + +syntax = "proto3"; +package org.apache.hertzbeat.common.entity.message; + +message Message +{ + // collector identity + string identity = 1; + // message direction + Direction direction = 2; + // message type + MessageType type = 3; + // message content + bytes msg = 4; +} + +enum MessageType +{ + // heartbeat message + HEARTBEAT = 0; + // collector go online to master message + GO_ONLINE = 1; + // collector go offline to master message + GO_OFFLINE = 2; + // collector go close to master + GO_CLOSE = 3; + // issue cyclic collect task + ISSUE_CYCLIC_TASK = 4; + // delete cyclic collect task + DELETE_CYCLIC_TASK = 5; + // issue one-time collect task + ISSUE_ONE_TIME_TASK = 6; + // response one-time collect data + RESPONSE_ONE_TIME_TASK_DATA = 7; + // response cyclic collect data + RESPONSE_CYCLIC_TASK_DATA = 8; + // response cyclic service discovery data + RESPONSE_CYCLIC_TASK_SD_DATA = 9; +} + +enum Direction { + // request message + REQUEST = 0; + // request response + RESPONSE = 1; +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/arrow/vector/table/ArrowTableTest.java b/hertzbeat-common-core/src/test/java/org/apache/arrow/vector/table/ArrowTableTest.java new file mode 100644 index 0000000..bc488ec --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/arrow/vector/table/ArrowTableTest.java @@ -0,0 +1,188 @@ +/* + * 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.arrow.vector.table; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.IntVector; +import org.apache.arrow.vector.VarCharVector; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.ipc.ArrowStreamReader; +import org.apache.arrow.vector.ipc.ArrowStreamWriter; +import org.apache.arrow.vector.types.pojo.ArrowType; +import org.apache.arrow.vector.types.pojo.Field; +import org.apache.arrow.vector.types.pojo.FieldType; +import org.apache.arrow.vector.types.pojo.Schema; +import org.junit.jupiter.api.Test; + +/** + * test for arrow table + */ +class ArrowTableTest { + + @Test + void testArrow() { + BufferAllocator allocator = new RootAllocator(); + Map<String, String> metadata = new HashMap<>(); + metadata.put("K1", "V1"); + metadata.put("K2", "V2"); + Field a = new Field("age", FieldType.nullable(new ArrowType.Int(32, true)), /*children*/ null); + Field b = new Field("name", FieldType.nullable(new ArrowType.Utf8()), /*children*/ null); + Schema schema = new Schema(asList(a, b), metadata); + VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator); + IntVector ageVector = (IntVector) root.getVector("age"); + VarCharVector nameVector = (VarCharVector) root.getVector("name"); + ageVector.allocateNew(3); + ageVector.set(0, 10); + ageVector.set(1, 20); + ageVector.set(2, 30); + nameVector.allocateNew(3); + nameVector.set(0, "Dave".getBytes(StandardCharsets.UTF_8)); + nameVector.set(1, "Peter".getBytes(StandardCharsets.UTF_8)); + nameVector.set(2, "Mary".getBytes(StandardCharsets.UTF_8)); + root.setRowCount(3); + + assertEquals(3, root.getRowCount()); + Table t = new Table(root); + assertEquals(3, t.getRowCount()); + Table t2 = t.slice(2); + root.close(); + VectorSchemaRoot root2 = t.toVectorSchemaRoot(); + assertEquals(3, root2.getRowCount()); + assertEquals(0, t.getRowCount()); + assertEquals(1, t2.getRowCount()); + assertEquals(0, root.getRowCount()); + root2.close(); + } + + @Test + void testArrow3() { + BufferAllocator allocator = new RootAllocator(); + Map<String, String> metadata = new HashMap<>(); + metadata.put("K1", "V1"); + metadata.put("K2", "V2"); + Field a = new Field("age", FieldType.nullable(new ArrowType.Int(32, true)), /*children*/ null); + Field b = new Field("name", FieldType.nullable(new ArrowType.Utf8()), /*children*/ null); + Schema schema = new Schema(asList(a, b), metadata); + VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator); + IntVector ageVector = (IntVector) root.getVector("age"); + VarCharVector nameVector = (VarCharVector) root.getVector("name"); + ageVector.allocateNew(3); + ageVector.set(0, 10); + ageVector.set(1, 20); + ageVector.set(2, 30); + nameVector.allocateNew(3); + nameVector.set(0, "Dave".getBytes(StandardCharsets.UTF_8)); + nameVector.set(1, "Peter".getBytes(StandardCharsets.UTF_8)); + nameVector.set(2, "Mary".getBytes(StandardCharsets.UTF_8)); + root.setRowCount(3); + + assertEquals(3, root.getRowCount()); + ArrowTable t = new ArrowTable(root); + assertEquals(3, t.getRowCount()); + VectorSchemaRoot root1 = t.toVectorSchemaRoot(); + assertEquals(3, root1.getRowCount()); + assertEquals("V1", root1.getSchema().getCustomMetadata().get("K1")); + ArrowTable t2 = new ArrowTable(root1); + assertEquals(3, t2.getRowCount()); + assertEquals("V1", t2.getSchema().getCustomMetadata().get("K1")); + VectorSchemaRoot root2 = t2.toVectorSchemaRoot(); + + byte[] bytes = null; + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArrowStreamWriter writer = new ArrowStreamWriter(root2, null, Channels.newChannel(out))) { + writer.start(); + writer.writeBatch(); + writer.end(); + bytes = out.toByteArray(); + } catch (IOException ignored) {} + assertEquals(3, root2.getRowCount()); + root2.close(); + BufferAllocator bufferAllocator = new RootAllocator(); + try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + ArrowStreamReader arrowStreamReader = new ArrowStreamReader(byteArrayInputStream, bufferAllocator)) { + arrowStreamReader.loadNextBatch(); + VectorSchemaRoot schemaRoot = arrowStreamReader.getVectorSchemaRoot(); + assertEquals(3, schemaRoot.getRowCount()); + assertEquals(schemaRoot.getSchema().getCustomMetadata().get("K1"), "V1"); + } catch (IOException e) { + throw new RuntimeException(e); + } + root.close(); + root2.close(); + t.close(); + t2.close(); + allocator.close(); + } + + @Test + void testArrow2() { + BufferAllocator allocator = new RootAllocator(); + Map<String, String> metadata = new HashMap<>(); + metadata.put("K1", "V1"); + metadata.put("K2", "V2"); + Field a = new Field("age", FieldType.nullable(new ArrowType.Int(32, true)), /*children*/ null); + Field b = new Field("name", FieldType.nullable(new ArrowType.Utf8()), /*children*/ null); + Schema schema = new Schema(asList(a, b), metadata); + VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator); + IntVector ageVector = (IntVector) root.getVector("age"); + VarCharVector nameVector = (VarCharVector) root.getVector("name"); + ageVector.allocateNew(3); + ageVector.set(0, 10); + ageVector.set(1, 20); + ageVector.set(2, 30); + nameVector.allocateNew(3); + nameVector.set(0, "Dave".getBytes(StandardCharsets.UTF_8)); + nameVector.set(1, "Peter".getBytes(StandardCharsets.UTF_8)); + nameVector.set(2, "Mary".getBytes(StandardCharsets.UTF_8)); + root.setRowCount(3); + + assertEquals(3, root.getRowCount()); + byte[] bytes = null; + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArrowStreamWriter writer = new ArrowStreamWriter(root, null, Channels.newChannel(out))) { + writer.start(); + writer.writeBatch(); + writer.end(); + bytes = out.toByteArray(); + } catch (IOException ignored) {} + assertEquals(3, root.getRowCount()); + root.close(); + BufferAllocator bufferAllocator = new RootAllocator(); + try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + ArrowStreamReader arrowStreamReader = new ArrowStreamReader(byteArrayInputStream, bufferAllocator)) { + arrowStreamReader.loadNextBatch(); + VectorSchemaRoot schemaRoot = arrowStreamReader.getVectorSchemaRoot(); + assertEquals(3, schemaRoot.getRowCount()); + assertEquals(schemaRoot.getSchema().getCustomMetadata().get("K1"), "V1"); + } catch (IOException e) { + throw new RuntimeException(e); + } + root.close(); + allocator.close(); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/concurrent/ManagedExecutorsTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/concurrent/ManagedExecutorsTest.java new file mode 100644 index 0000000..9f104b5 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/concurrent/ManagedExecutorsTest.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.concurrent; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link ManagedExecutors}. + */ +class ManagedExecutorsTest { + + @Test + void shouldRunTaskOnVirtualThread() throws Exception { + ManagedExecutor executor = ManagedExecutors.newVirtualExecutor("test", "test-vt-", + AdmissionMode.UNBOUNDED_VT, 0, (thread, throwable) -> { + }); + try { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + + executor.execute(() -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } finally { + executor.close(); + } + } + + @Test + void shouldRejectTaskWhenAdmissionLimitReached() throws Exception { + ManagedExecutor executor = ManagedExecutors.newVirtualExecutor("limited", "limited-vt-", + AdmissionMode.LIMIT_AND_REJECT, 1, (thread, throwable) -> { + }); + CountDownLatch started = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + try { + executor.execute(() -> { + started.countDown(); + try { + release.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertTrue(started.await(5, TimeUnit.SECONDS)); + assertThrows(RejectedExecutionException.class, () -> executor.execute(() -> { + })); + } finally { + release.countDown(); + executor.close(); + } + } + + @Test + void shouldQueueTasksWhileKeepingVirtualThreadExecution() throws Exception { + ManagedExecutor executor = ManagedExecutors.newQueuedVirtualExecutor("queued", "queued-vt-", + 1, 0, (thread, throwable) -> { + }); + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + AtomicBoolean firstVirtual = new AtomicBoolean(false); + AtomicBoolean secondVirtual = new AtomicBoolean(false); + try { + executor.execute(() -> { + firstVirtual.set(Thread.currentThread().isVirtual()); + firstStarted.countDown(); + try { + releaseFirst.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + executor.execute(() -> { + secondVirtual.set(Thread.currentThread().isVirtual()); + secondStarted.countDown(); + }); + + assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS)); + releaseFirst.countDown(); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + assertTrue(firstVirtual.get()); + assertTrue(secondVirtual.get()); + } finally { + releaseFirst.countDown(); + executor.close(); + } + } + + @Test + void shouldRejectTaskWhenQueuedExecutorCapacityReached() throws Exception { + ManagedExecutor executor = ManagedExecutors.newQueuedVirtualExecutor("queued", "queued-vt-", + 1, 1, (thread, throwable) -> { + }); + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + try { + executor.execute(() -> { + firstStarted.countDown(); + try { + releaseFirst.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + executor.execute(secondStarted::countDown); + assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS)); + assertThrows(RejectedExecutionException.class, () -> executor.execute(() -> { + })); + } finally { + releaseFirst.countDown(); + executor.close(); + } + } + + @Test + void shouldDiscardOldestTaskWhenDiscardOldestExecutorQueueIsFull() throws Exception { + ManagedExecutor executor = ManagedExecutors.newDiscardOldestVirtualExecutor("discard-oldest", + "discard-oldest-vt-", 1, 1, 1, (thread, throwable) -> { + }); + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch thirdStarted = new CountDownLatch(1); + AtomicBoolean secondExecuted = new AtomicBoolean(false); + AtomicBoolean thirdVirtual = new AtomicBoolean(false); + try { + executor.execute(() -> { + firstStarted.countDown(); + try { + releaseFirst.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + executor.execute(() -> secondExecuted.set(true)); + executor.execute(() -> { + thirdVirtual.set(Thread.currentThread().isVirtual()); + thirdStarted.countDown(); + }); + + releaseFirst.countDown(); + assertTrue(thirdStarted.await(5, TimeUnit.SECONDS)); + assertFalse(secondExecuted.get()); + assertTrue(thirdVirtual.get()); + } finally { + releaseFirst.countDown(); + executor.close(); + } + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/DnsSdProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/DnsSdProtocolTest.java new file mode 100644 index 0000000..d3e9097 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/DnsSdProtocolTest.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DnsSdProtocolTest { + + @Test + void isInvalidValidProtocol() { + DnsSdProtocol protocol = DnsSdProtocol.builder() + .host("192.168.1.1") + .port("53") + .recordType("A") + .recordName("example.com") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithIpv6() { + DnsSdProtocol protocol = DnsSdProtocol.builder() + .host("::1") + .port("53") + .recordType("AAAA") + .recordName("example.com") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + DnsSdProtocol protocol = DnsSdProtocol.builder() + .host("localhost") + .port("53") + .recordType("CNAME") + .recordName("www.example.com") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + DnsSdProtocol protocol = DnsSdProtocol.builder() + .host("") + .port("53") + .recordType("A") + .recordName("example.com") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + DnsSdProtocol protocol = DnsSdProtocol.builder() + .host("192.168.1.1") + .port("99999") + .recordType("A") + .recordName("example.com") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankRecordType() { + DnsSdProtocol protocol = DnsSdProtocol.builder() + .host("192.168.1.1") + .port("53") + .recordType("") + .recordName("example.com") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankRecordName() { + DnsSdProtocol protocol = DnsSdProtocol.builder() + .host("192.168.1.1") + .port("53") + .recordType("A") + .recordName("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidRecordType() { + DnsSdProtocol protocol = DnsSdProtocol.builder() + .host("192.168.1.1") + .port("53") + .recordType("INVALID") + .recordName("example.com") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidRecordTypes() { + String[] validTypes = {"A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV", "PTR", "SOA", "CAA"}; + for (String type : validTypes) { + DnsSdProtocol protocol = DnsSdProtocol.builder() + .host("192.168.1.1") + .port("53") + .recordType(type) + .recordName("example.com") + .build(); + assertFalse(protocol.isInvalid(), "Record type " + type + " should be valid"); + } + } + + @Test + void isInvalidValidRecordTypesCaseInsensitive() { + DnsSdProtocol protocol = DnsSdProtocol.builder() + .host("192.168.1.1") + .port("53") + .recordType("a") + .recordName("example.com") + .build(); + assertFalse(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/EurekaSdProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/EurekaSdProtocolTest.java new file mode 100644 index 0000000..634eaad --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/EurekaSdProtocolTest.java @@ -0,0 +1,66 @@ +/* + * 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.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class EurekaSdProtocolTest { + + @Test + void isInvalidValidHttpUrl() { + EurekaSdProtocol protocol = EurekaSdProtocol.builder() + .url("http://127.0.0.1:8761/eureka") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidHttpsUrl() { + EurekaSdProtocol protocol = EurekaSdProtocol.builder() + .url("https://eureka.example.com/eureka") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankUrl() { + EurekaSdProtocol protocol = EurekaSdProtocol.builder() + .url("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUnsupportedScheme() { + EurekaSdProtocol protocol = EurekaSdProtocol.builder() + .url("ftp://eureka.example.com/eureka") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidMalformedUrl() { + EurekaSdProtocol protocol = EurekaSdProtocol.builder() + .url("http://") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocolTest.java new file mode 100644 index 0000000..b17795b --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/FtpProtocolTest.java @@ -0,0 +1,98 @@ +/* + * 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.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class FtpProtocolTest { + + @Test + void isInvalidValidAnonymousFtp() { + FtpProtocol protocol = FtpProtocol.builder() + .host("ftp.example.com") + .port("21") + .direction("/") + .timeout("3000") + .ssl("false") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidSftp() { + FtpProtocol protocol = FtpProtocol.builder() + .host("sftp.example.com") + .port("22") + .direction("/data") + .timeout("3000") + .ssl("true") + .username("admin") + .password("secret") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidMissingDirection() { + FtpProtocol protocol = FtpProtocol.builder() + .host("ftp.example.com") + .port("21") + .timeout("3000") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUnsupportedSslValue() { + FtpProtocol protocol = FtpProtocol.builder() + .host("ftp.example.com") + .port("21") + .direction("/") + .timeout("3000") + .ssl("yes") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidSftpWithoutPassword() { + FtpProtocol protocol = FtpProtocol.builder() + .host("sftp.example.com") + .port("22") + .direction("/data") + .timeout("3000") + .ssl("true") + .username("admin") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + FtpProtocol protocol = FtpProtocol.builder() + .host("ftp.example.com") + .port("21") + .direction("/") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/HttpProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/HttpProtocolTest.java new file mode 100644 index 0000000..a185b22 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/HttpProtocolTest.java @@ -0,0 +1,240 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HttpProtocolTest { + + @Test + void isInvalidValidProtocol() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + HttpProtocol protocol = HttpProtocol.builder() + .host("example.com") + .port("8080") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + HttpProtocol protocol = HttpProtocol.builder() + .host("localhost") + .port("8080") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithIpv6() { + HttpProtocol protocol = HttpProtocol.builder() + .host("::1") + .port("8080") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + HttpProtocol protocol = HttpProtocol.builder() + .host("") + .port("8080") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + HttpProtocol protocol = HttpProtocol.builder() + .host(null) + .port("8080") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullTimeout() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .timeout(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidSsl() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .ssl("true") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidSslFalse() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .ssl("false") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidSsl() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .ssl("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankSsl() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .ssl("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidMethod() { + String[] validMethods = {"get", "GET", "post", "POST", "put", "PUT", "delete", "DELETE", "patch", "PATCH", "head", "HEAD", "options", "OPTIONS"}; + for (String method : validMethods) { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .method(method) + .build(); + assertFalse(protocol.isInvalid(), "Method " + method + " should be valid"); + } + } + + @Test + void isInvalidInvalidMethod() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .method("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankMethod() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .method("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidEnableUrlEncoding() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .enableUrlEncoding("true") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidEnableUrlEncoding() { + HttpProtocol protocol = HttpProtocol.builder() + .host("192.168.1.1") + .port("8080") + .enableUrlEncoding("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/HttpSdProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/HttpSdProtocolTest.java new file mode 100644 index 0000000..0a91759 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/HttpSdProtocolTest.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HttpSdProtocolTest { + + @Test + void isInvalidValidHttpUrl() { + HttpSdProtocol protocol = HttpSdProtocol.builder() + .url("http://example.com/api/discovery") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidHttpsUrl() { + HttpSdProtocol protocol = HttpSdProtocol.builder() + .url("https://example.com/api/discovery") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidUrlWithPort() { + HttpSdProtocol protocol = HttpSdProtocol.builder() + .url("http://example.com:8080/api/discovery") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankUrl() { + HttpSdProtocol protocol = HttpSdProtocol.builder() + .url("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullUrl() { + HttpSdProtocol protocol = HttpSdProtocol.builder() + .url(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidUrlNoProtocol() { + HttpSdProtocol protocol = HttpSdProtocol.builder() + .url("example.com/api/discovery") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidUrlWrongProtocol() { + HttpSdProtocol protocol = HttpSdProtocol.builder() + .url("ftp://example.com/api/discovery") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidUrlOnlyProtocol() { + HttpSdProtocol protocol = HttpSdProtocol.builder() + .url("http://") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/IcmpProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/IcmpProtocolTest.java new file mode 100644 index 0000000..0d9d330 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/IcmpProtocolTest.java @@ -0,0 +1,118 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class IcmpProtocolTest { + + @Test + void isInvalidValidProtocol() { + IcmpProtocol protocol = IcmpProtocol.builder() + .host("192.168.1.1") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + IcmpProtocol protocol = IcmpProtocol.builder() + .host("example.com") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + IcmpProtocol protocol = IcmpProtocol.builder() + .host("localhost") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithIpv6() { + IcmpProtocol protocol = IcmpProtocol.builder() + .host("::1") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + IcmpProtocol protocol = IcmpProtocol.builder() + .host("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + IcmpProtocol protocol = IcmpProtocol.builder() + .host(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidDomain() { + IcmpProtocol protocol = IcmpProtocol.builder() + .host("invalid..domain") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + IcmpProtocol protocol = IcmpProtocol.builder() + .host("192.168.1.1") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + IcmpProtocol protocol = IcmpProtocol.builder() + .host("192.168.1.1") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullTimeout() { + IcmpProtocol protocol = IcmpProtocol.builder() + .host("192.168.1.1") + .timeout(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + IcmpProtocol protocol = IcmpProtocol.builder() + .host("192.168.1.1") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/ImapProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/ImapProtocolTest.java new file mode 100644 index 0000000..d567a6c --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/ImapProtocolTest.java @@ -0,0 +1,93 @@ +/* + * 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.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ImapProtocolTest { + + @Test + void isInvalidValidProtocol() { + ImapProtocol protocol = ImapProtocol.builder() + .host("imap.example.com") + .port("993") + .timeout("6000") + .ssl("true") + .email("test@example.com") + .authorize("auth-code") + .folderName("INBOX") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidMissingFolderName() { + ImapProtocol protocol = ImapProtocol.builder() + .host("imap.example.com") + .port("993") + .timeout("6000") + .email("test@example.com") + .authorize("auth-code") + .folderName("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUnsupportedSslValue() { + ImapProtocol protocol = ImapProtocol.builder() + .host("imap.example.com") + .port("993") + .timeout("6000") + .ssl("yes") + .email("test@example.com") + .authorize("auth-code") + .folderName("INBOX") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + ImapProtocol protocol = ImapProtocol.builder() + .host("imap.example.com") + .port("993") + .timeout("abc") + .email("test@example.com") + .authorize("auth-code") + .folderName("INBOX") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + ImapProtocol protocol = ImapProtocol.builder() + .host("") + .port("993") + .timeout("6000") + .email("test@example.com") + .authorize("auth-code") + .folderName("INBOX") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/IpmiProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/IpmiProtocolTest.java new file mode 100644 index 0000000..12f883c --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/IpmiProtocolTest.java @@ -0,0 +1,135 @@ +/* + * 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.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class IpmiProtocolTest { + + @Test + void isInvalidValidProtocol() { + IpmiProtocol protocol = IpmiProtocol.builder() + .host("192.168.1.1") + .port("623") + .username("admin") + .password("password") + .type("Chassis") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidRawProtocol() { + IpmiProtocol protocol = IpmiProtocol.builder() + .host("192.168.1.1") + .port("623") + .username("admin") + .password("password") + .type("Raw") + .field(new IpmiProtocol.Field()) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + IpmiProtocol protocol = IpmiProtocol.builder() + .host("") + .port("623") + .username("admin") + .password("password") + .type("Chassis") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + IpmiProtocol protocol = IpmiProtocol.builder() + .host("192.168.1.1") + .port("70000") + .username("admin") + .password("password") + .type("Chassis") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankUsername() { + IpmiProtocol protocol = IpmiProtocol.builder() + .host("192.168.1.1") + .port("623") + .username("") + .password("password") + .type("Chassis") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPassword() { + IpmiProtocol protocol = IpmiProtocol.builder() + .host("192.168.1.1") + .port("623") + .username("admin") + .password("") + .type("Chassis") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankType() { + IpmiProtocol protocol = IpmiProtocol.builder() + .host("192.168.1.1") + .port("623") + .username("admin") + .password("password") + .type("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUnsupportedType() { + IpmiProtocol protocol = IpmiProtocol.builder() + .host("192.168.1.1") + .port("623") + .username("admin") + .password("password") + .type("Unknown") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidRawWithoutField() { + IpmiProtocol protocol = IpmiProtocol.builder() + .host("192.168.1.1") + .port("623") + .username("admin") + .password("password") + .type("Raw") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/JdbcProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/JdbcProtocolTest.java new file mode 100644 index 0000000..9341ee8 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/JdbcProtocolTest.java @@ -0,0 +1,185 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JdbcProtocolTest { + + @Test + void isInvalidValidProtocol() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("192.168.1.1") + .port("3306") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("db.example.com") + .port("3306") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("localhost") + .port("3306") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithIpv6() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("::1") + .port("3306") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithUrl() { + JdbcProtocol protocol = JdbcProtocol.builder() + .url("jdbc:mysql://localhost:3306/test") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("") + .port("3306") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host(null) + .port("3306") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("192.168.1.1") + .port("3306") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("192.168.1.1") + .port("3306") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullTimeout() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("192.168.1.1") + .port("3306") + .timeout(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("192.168.1.1") + .port("3306") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidReuseConnection() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("192.168.1.1") + .port("3306") + .reuseConnection("true") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidReuseConnectionFalse() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("192.168.1.1") + .port("3306") + .reuseConnection("false") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidReuseConnection() { + JdbcProtocol protocol = JdbcProtocol.builder() + .host("192.168.1.1") + .port("3306") + .reuseConnection("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/JmxProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/JmxProtocolTest.java new file mode 100644 index 0000000..bfd2fb4 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/JmxProtocolTest.java @@ -0,0 +1,128 @@ +/* + * 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.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class JmxProtocolTest { + + @Test + void isInvalidValidHostPortProtocol() { + JmxProtocol protocol = JmxProtocol.builder() + .host("192.168.1.1") + .port("9999") + .objectName("java.lang:type=Runtime") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidUrlProtocol() { + JmxProtocol protocol = JmxProtocol.builder() + .url("service:jmx:rmi:///jndi/rmi://127.0.0.1:9999/jmxrmi") + .objectName("java.lang:type=Runtime") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankObjectName() { + JmxProtocol protocol = JmxProtocol.builder() + .host("192.168.1.1") + .port("9999") + .objectName("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + JmxProtocol protocol = JmxProtocol.builder() + .host("") + .port("9999") + .objectName("java.lang:type=Runtime") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidZeroPort() { + JmxProtocol protocol = JmxProtocol.builder() + .host("192.168.1.1") + .port("0") + .objectName("java.lang:type=Runtime") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUnsupportedSsl() { + JmxProtocol protocol = JmxProtocol.builder() + .host("192.168.1.1") + .port("9999") + .objectName("java.lang:type=Runtime") + .ssl("yes") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidOnlyUsername() { + JmxProtocol protocol = JmxProtocol.builder() + .host("192.168.1.1") + .port("9999") + .objectName("java.lang:type=Runtime") + .username("admin") + .password("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidOnlyPassword() { + JmxProtocol protocol = JmxProtocol.builder() + .host("192.168.1.1") + .port("9999") + .objectName("java.lang:type=Runtime") + .username("") + .password("password") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUrlProtocolMismatch() { + JmxProtocol protocol = JmxProtocol.builder() + .url("http://127.0.0.1:9999/jmxrmi") + .objectName("java.lang:type=Runtime") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUrlContainsStub() { + JmxProtocol protocol = JmxProtocol.builder() + .url("service:jmx:rmi:///jndi/rmi://127.0.0.1:9999/stub/path") + .objectName("java.lang:type=Runtime") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/KafkaProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/KafkaProtocolTest.java new file mode 100644 index 0000000..368592a --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/KafkaProtocolTest.java @@ -0,0 +1,178 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class KafkaProtocolTest { + + @Test + void isInvalidValidProtocol() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("192.168.1.1") + .port("9092") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("kafka.example.com") + .port("9092") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("localhost") + .port("9092") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("") + .port("9092") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host(null) + .port("9092") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("192.168.1.1") + .port("9092") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("192.168.1.1") + .port("9092") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullTimeout() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("192.168.1.1") + .port("9092") + .timeout(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("192.168.1.1") + .port("9092") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidMonitorInternalTopicTrue() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("192.168.1.1") + .port("9092") + .monitorInternalTopic("true") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidMonitorInternalTopicFalse() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("192.168.1.1") + .port("9092") + .monitorInternalTopic("false") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidMonitorInternalTopic() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("192.168.1.1") + .port("9092") + .monitorInternalTopic("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankMonitorInternalTopic() { + KafkaProtocol protocol = KafkaProtocol.builder() + .host("192.168.1.1") + .port("9092") + .monitorInternalTopic("") + .build(); + assertFalse(protocol.isInvalid()); + } +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/MemcachedProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/MemcachedProtocolTest.java new file mode 100644 index 0000000..59b1ff0 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/MemcachedProtocolTest.java @@ -0,0 +1,107 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MemcachedProtocolTest { + + @Test + void isInvalidValidProtocol() { + MemcachedProtocol protocol = MemcachedProtocol.builder() + .host("192.168.1.1") + .port("11211") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + MemcachedProtocol protocol = MemcachedProtocol.builder() + .host("memcached.example.com") + .port("11211") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + MemcachedProtocol protocol = MemcachedProtocol.builder() + .host("localhost") + .port("11211") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithIpv6() { + MemcachedProtocol protocol = MemcachedProtocol.builder() + .host("::1") + .port("11211") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + MemcachedProtocol protocol = MemcachedProtocol.builder() + .host("") + .port("11211") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + MemcachedProtocol protocol = MemcachedProtocol.builder() + .host(null) + .port("11211") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + MemcachedProtocol protocol = MemcachedProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + MemcachedProtocol protocol = MemcachedProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + MemcachedProtocol protocol = MemcachedProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/ModbusProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/ModbusProtocolTest.java new file mode 100644 index 0000000..e5a9369 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/ModbusProtocolTest.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ModbusProtocolTest { + + @Test + void isInvalidValidProtocol() { + ModbusProtocol protocol = ModbusProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .registerAddresses(Arrays.asList("40001", "40002")) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithTimeout() { + ModbusProtocol protocol = ModbusProtocol.builder() + .host("localhost") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .timeout("5000") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + ModbusProtocol protocol = ModbusProtocol.builder() + .host("") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + ModbusProtocol protocol = ModbusProtocol.builder() + .host("192.168.1.1") + .port("99999") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankDriverName() { + ModbusProtocol protocol = ModbusProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("") + .addressSyntax("Modbus") + .slaveId("1") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankAddressSyntax() { + ModbusProtocol protocol = ModbusProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("modbus") + .addressSyntax("") + .slaveId("1") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankSlaveId() { + ModbusProtocol protocol = ModbusProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNonNumericTimeout() { + ModbusProtocol protocol = ModbusProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .timeout("abc") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidEmptyRegisterAddresses() { + ModbusProtocol protocol = ModbusProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .registerAddresses(Collections.emptyList()) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullRegisterAddresses() { + ModbusProtocol protocol = ModbusProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .registerAddresses(null) + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/MongodbProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/MongodbProtocolTest.java new file mode 100644 index 0000000..1fd0816 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/MongodbProtocolTest.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MongodbProtocolTest { + + @Test + void isInvalidValidProtocol() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("192.168.1.1") + .port("27017") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("mongodb.example.com") + .port("27017") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("localhost") + .port("27017") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithIpv6() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("::1") + .port("27017") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("") + .port("27017") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host(null) + .port("27017") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("192.168.1.1") + .port("27017") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("192.168.1.1") + .port("27017") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullTimeout() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("192.168.1.1") + .port("27017") + .timeout(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("192.168.1.1") + .port("27017") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithUsername() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("192.168.1.1") + .port("27017") + .username("admin") + .password("password") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDatabase() { + MongodbProtocol protocol = MongodbProtocol.builder() + .host("192.168.1.1") + .port("27017") + .database("testdb") + .build(); + assertFalse(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/MqttProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/MqttProtocolTest.java new file mode 100644 index 0000000..15b10a2 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/MqttProtocolTest.java @@ -0,0 +1,322 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MqttProtocolTest { + + @Test + void isInvalidValidProtocol() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("1883") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + MqttProtocol protocol = MqttProtocol.builder() + .host("mqtt.example.com") + .port("1883") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + MqttProtocol protocol = MqttProtocol.builder() + .host("localhost") + .port("1883") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + MqttProtocol protocol = MqttProtocol.builder() + .host("") + .port("1883") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + MqttProtocol protocol = MqttProtocol.builder() + .host(null) + .port("1883") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("1883") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("1883") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("1883") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidKeepalive() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("1883") + .keepalive("60") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidKeepalive() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("1883") + .keepalive("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolMqtt() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("1883") + .protocol("MQTT") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolMqtts() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("8883") + .protocol("MQTTS") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidProtocol() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("1883") + .protocol("INVALID") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTlsVersion() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("8883") + .tlsVersion("TLSv1.2") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidTlsVersionV13() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("8883") + .tlsVersion("TLSv1.3") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTlsVersion() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("8883") + .tlsVersion("TLSv1.0") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidInsecureSkipVerify() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("8883") + .insecureSkipVerify("true") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidInsecureSkipVerify() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("8883") + .insecureSkipVerify("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidEnableMutualAuth() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("8883") + .enableMutualAuth("true") + .clientCert("/path/to/cert") + .clientKey("/path/to/key") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidEnableMutualAuthWithoutClientCert() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("8883") + .enableMutualAuth("true") + .clientKey("/path/to/key") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidEnableMutualAuthWithoutClientKey() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("8883") + .enableMutualAuth("true") + .clientCert("/path/to/cert") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidEnableMutualAuthFalseWithoutCert() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("8883") + .enableMutualAuth("false") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidEnableMutualAuth() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("8883") + .enableMutualAuth("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidAuth() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("1883") + .username("user") + .password("pass") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidOnlyUsername() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("1883") + .username("user") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidOnlyPassword() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("1883") + .password("pass") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNoAuth() { + MqttProtocol protocol = MqttProtocol.builder() + .host("192.168.1.1") + .port("1883") + .build(); + assertFalse(protocol.isInvalid()); + } +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/NebulaGraphProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/NebulaGraphProtocolTest.java new file mode 100644 index 0000000..e589080 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/NebulaGraphProtocolTest.java @@ -0,0 +1,188 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class NebulaGraphProtocolTest { + + @Test + void isInvalidValidProtocol() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port("19669") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("nebula.example.com") + .port("19669") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("localhost") + .port("19669") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("") + .port("19669") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host(null) + .port("19669") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port("19669") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port("19669") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullTimeout() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port("19669") + .timeout(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port("19669") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimePeriod() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port("19669") + .timePeriod("60") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimePeriod() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port("19669") + .timePeriod("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimePeriod() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port("19669") + .timePeriod("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidWithUrl() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port("19669") + .url("/stats") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidStoragePort() { + NebulaGraphProtocol protocol = NebulaGraphProtocol.builder() + .host("192.168.1.1") + .port("19779") + .url("/rocksdb_stats") + .build(); + assertFalse(protocol.isInvalid()); + } +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/NginxProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/NginxProtocolTest.java new file mode 100644 index 0000000..dc13786 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/NginxProtocolTest.java @@ -0,0 +1,197 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class NginxProtocolTest { + + @Test + void isInvalidValidProtocol() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port("80") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + NginxProtocol protocol = NginxProtocol.builder() + .host("nginx.example.com") + .port("80") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + NginxProtocol protocol = NginxProtocol.builder() + .host("localhost") + .port("80") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + NginxProtocol protocol = NginxProtocol.builder() + .host("") + .port("80") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + NginxProtocol protocol = NginxProtocol.builder() + .host(null) + .port("80") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port("80") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port("80") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullTimeout() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port("80") + .timeout(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port("80") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidSslTrue() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port("443") + .ssl("true") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidSslFalse() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port("80") + .ssl("false") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidSsl() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port("80") + .ssl("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankSsl() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port("80") + .ssl("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidWithUrl() { + NginxProtocol protocol = NginxProtocol.builder() + .host("192.168.1.1") + .port("80") + .url("/nginx_status") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidIpv6() { + NginxProtocol protocol = NginxProtocol.builder() + .host("::1") + .port("80") + .build(); + assertFalse(protocol.isInvalid()); + } +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/NgqlProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/NgqlProtocolTest.java new file mode 100644 index 0000000..98e89ea --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/NgqlProtocolTest.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class NgqlProtocolTest { + + @Test + void isInvalidValidProtocol() { + NgqlProtocol protocol = NgqlProtocol.builder() + .host("127.0.0.1") + .port("9669") + .username("root") + .password("nebula") + .timeout("6000") + .parseType("oneRow") + .commands(List.of("SHOW HOSTS;")) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidUnsupportedParseType() { + NgqlProtocol protocol = NgqlProtocol.builder() + .host("127.0.0.1") + .port("9669") + .username("root") + .password("nebula") + .timeout("6000") + .parseType("json") + .commands(List.of("SHOW HOSTS;")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidEmptyCommands() { + NgqlProtocol protocol = NgqlProtocol.builder() + .host("127.0.0.1") + .port("9669") + .username("root") + .password("nebula") + .timeout("6000") + .parseType("oneRow") + .commands(List.of()) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankCommandItem() { + NgqlProtocol protocol = NgqlProtocol.builder() + .host("127.0.0.1") + .port("9669") + .username("root") + .password("nebula") + .timeout("6000") + .parseType("oneRow") + .commands(List.of(" ")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + NgqlProtocol protocol = NgqlProtocol.builder() + .host("127.0.0.1") + .port("9669") + .username("root") + .password("nebula") + .timeout("abc") + .parseType("oneRow") + .commands(List.of("SHOW HOSTS;")) + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/NtpProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/NtpProtocolTest.java new file mode 100644 index 0000000..a000b79 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/NtpProtocolTest.java @@ -0,0 +1,80 @@ +/* + * 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.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class NtpProtocolTest { + + @Test + void isInvalidValidProtocolWithoutPort() { + NtpProtocol protocol = NtpProtocol.builder() + .host("pool.ntp.org") + .timeout("3000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithPort() { + NtpProtocol protocol = NtpProtocol.builder() + .host("192.168.1.1") + .port("123") + .timeout("3000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithoutTimeout() { + NtpProtocol protocol = NtpProtocol.builder() + .host("pool.ntp.org") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + NtpProtocol protocol = NtpProtocol.builder() + .host("") + .timeout("3000") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + NtpProtocol protocol = NtpProtocol.builder() + .host("pool.ntp.org") + .port("70000") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + NtpProtocol protocol = NtpProtocol.builder() + .host("pool.ntp.org") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/PlcProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/PlcProtocolTest.java new file mode 100644 index 0000000..0eb4393 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/PlcProtocolTest.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PlcProtocolTest { + + @Test + void isInvalidValidProtocol() { + PlcProtocol protocol = PlcProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .registerAddresses(Arrays.asList("40001", "40002")) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithTimeout() { + PlcProtocol protocol = PlcProtocol.builder() + .host("localhost") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .timeout("5000") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + PlcProtocol protocol = PlcProtocol.builder() + .host("") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + PlcProtocol protocol = PlcProtocol.builder() + .host("192.168.1.1") + .port("99999") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankDriverName() { + PlcProtocol protocol = PlcProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("") + .addressSyntax("Modbus") + .slaveId("1") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankAddressSyntax() { + PlcProtocol protocol = PlcProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("modbus") + .addressSyntax("") + .slaveId("1") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankSlaveId() { + PlcProtocol protocol = PlcProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNonNumericTimeout() { + PlcProtocol protocol = PlcProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .timeout("abc") + .registerAddresses(Arrays.asList("40001")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidEmptyRegisterAddresses() { + PlcProtocol protocol = PlcProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .registerAddresses(Collections.emptyList()) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullRegisterAddresses() { + PlcProtocol protocol = PlcProtocol.builder() + .host("192.168.1.1") + .port("502") + .driverName("modbus") + .addressSyntax("Modbus") + .slaveId("1") + .registerAddresses(null) + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/PrometheusProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/PrometheusProtocolTest.java new file mode 100644 index 0000000..9390dd0 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/PrometheusProtocolTest.java @@ -0,0 +1,230 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PrometheusProtocolTest { + + @Test + void isInvalidValidProtocol() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("prometheus.example.com") + .port("9090") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("localhost") + .port("9090") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithIpv6() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("::1") + .port("9090") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("") + .port("9090") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host(null) + .port("9090") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullTimeout() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .timeout(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidSsl() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .ssl("true") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidSslFalse() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .ssl("false") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidSsl() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .ssl("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankSsl() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .ssl("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidMethod() { + String[] validMethods = {"get", "GET", "post", "POST", "put", "PUT", "delete", "DELETE", "patch", "PATCH", "head", "HEAD", "options", "OPTIONS"}; + for (String method : validMethods) { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .method(method) + .build(); + assertFalse(protocol.isInvalid(), "Method " + method + " should be valid"); + } + } + + @Test + void isInvalidInvalidMethod() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .method("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankMethod() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .method("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidPath() { + PrometheusProtocol protocol = PrometheusProtocol.builder() + .host("192.168.1.1") + .port("9090") + .path("/metrics") + .build(); + assertFalse(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/PushProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/PushProtocolTest.java new file mode 100644 index 0000000..eb139d8 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/PushProtocolTest.java @@ -0,0 +1,138 @@ +/* + * 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.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.apache.hertzbeat.common.entity.dto.Field; +import org.junit.jupiter.api.Test; + +class PushProtocolTest { + + @Test + void isInvalidValidProtocol() { + PushProtocol protocol = PushProtocol.builder() + .host("127.0.0.1") + .port("1157") + .uri("/api/push") + .fields(List.of(Field.builder().name("cpuUsage").type((byte) 0).build())) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithSchemaHost() { + PushProtocol protocol = PushProtocol.builder() + .host("http://127.0.0.1") + .port("1157") + .uri("/api/push") + .fields(List.of(Field.builder().name("status").type((byte) 1).build())) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + PushProtocol protocol = PushProtocol.builder() + .host("") + .port("1157") + .uri("/api/push") + .fields(List.of(Field.builder().name("status").type((byte) 1).build())) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidZeroPort() { + PushProtocol protocol = PushProtocol.builder() + .host("127.0.0.1") + .port("0") + .uri("/api/push") + .fields(List.of(Field.builder().name("status").type((byte) 1).build())) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankUri() { + PushProtocol protocol = PushProtocol.builder() + .host("127.0.0.1") + .port("1157") + .uri("") + .fields(List.of(Field.builder().name("status").type((byte) 1).build())) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUriWithoutLeadingSlash() { + PushProtocol protocol = PushProtocol.builder() + .host("127.0.0.1") + .port("1157") + .uri("api/push") + .fields(List.of(Field.builder().name("status").type((byte) 1).build())) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankFields() { + PushProtocol protocol = PushProtocol.builder() + .host("127.0.0.1") + .port("1157") + .uri("/api/push") + .fields(List.of()) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidFieldWithoutName() { + PushProtocol protocol = PushProtocol.builder() + .host("127.0.0.1") + .port("1157") + .uri("/api/push") + .fields(List.of(Field.builder().name("").type((byte) 1).build())) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidFieldWithoutType() { + PushProtocol protocol = PushProtocol.builder() + .host("127.0.0.1") + .port("1157") + .uri("/api/push") + .fields(List.of(Field.builder().name("status").type(null).build())) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidFieldWithUnsupportedType() { + PushProtocol protocol = PushProtocol.builder() + .host("127.0.0.1") + .port("1157") + .uri("/api/push") + .fields(List.of(Field.builder().name("status").type((byte) 2).build())) + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/RedfishProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/RedfishProtocolTest.java new file mode 100644 index 0000000..9a8bebe --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/RedfishProtocolTest.java @@ -0,0 +1,171 @@ +/* + * 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.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class RedfishProtocolTest { + + @Test + void isInvalidValidProtocol() { + RedfishProtocol protocol = RedfishProtocol.builder() + .host("127.0.0.1") + .port("443") + .username("Administrator") + .password("Password") + .timeout("5000") + .jsonPath(List.of("$.Id")) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithSchemaHost() { + RedfishProtocol protocol = RedfishProtocol.builder() + .host("https://127.0.0.1") + .port("443") + .username("Administrator") + .password("Password") + .timeout("5000") + .jsonPath(List.of("$.Id")) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + RedfishProtocol protocol = RedfishProtocol.builder() + .host("") + .port("443") + .username("Administrator") + .password("Password") + .timeout("5000") + .jsonPath(List.of("$.Id")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidZeroPort() { + RedfishProtocol protocol = RedfishProtocol.builder() + .host("127.0.0.1") + .port("0") + .username("Administrator") + .password("Password") + .timeout("5000") + .jsonPath(List.of("$.Id")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankUsername() { + RedfishProtocol protocol = RedfishProtocol.builder() + .host("127.0.0.1") + .port("443") + .username("") + .password("Password") + .timeout("5000") + .jsonPath(List.of("$.Id")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPassword() { + RedfishProtocol protocol = RedfishProtocol.builder() + .host("127.0.0.1") + .port("443") + .username("Administrator") + .password("") + .timeout("5000") + .jsonPath(List.of("$.Id")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + RedfishProtocol protocol = RedfishProtocol.builder() + .host("127.0.0.1") + .port("443") + .username("Administrator") + .password("Password") + .timeout("") + .jsonPath(List.of("$.Id")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNonNumericTimeout() { + RedfishProtocol protocol = RedfishProtocol.builder() + .host("127.0.0.1") + .port("443") + .username("Administrator") + .password("Password") + .timeout("5s") + .jsonPath(List.of("$.Id")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidSchema() { + RedfishProtocol protocol = RedfishProtocol.builder() + .host("127.0.0.1") + .port("443") + .username("Administrator") + .password("Password") + .timeout("5000") + .schema("redfish/v1/Chassis") + .jsonPath(List.of("$.Id")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankJsonPath() { + RedfishProtocol protocol = RedfishProtocol.builder() + .host("127.0.0.1") + .port("443") + .username("Administrator") + .password("Password") + .timeout("5000") + .jsonPath(List.of()) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidJsonPathContainsBlankItem() { + RedfishProtocol protocol = RedfishProtocol.builder() + .host("127.0.0.1") + .port("443") + .username("Administrator") + .password("Password") + .timeout("5000") + .jsonPath(List.of("$.Id", "")) + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/RedisProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/RedisProtocolTest.java new file mode 100644 index 0000000..33a8e61 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/RedisProtocolTest.java @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RedisProtocolTest { + + @Test + void isInvalidValidProtocol() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("6379") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + RedisProtocol protocol = RedisProtocol.builder() + .host("redis.example.com") + .port("6379") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + RedisProtocol protocol = RedisProtocol.builder() + .host("localhost") + .port("6379") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithIpv6() { + RedisProtocol protocol = RedisProtocol.builder() + .host("::1") + .port("6379") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + RedisProtocol protocol = RedisProtocol.builder() + .host("") + .port("6379") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + RedisProtocol protocol = RedisProtocol.builder() + .host(null) + .port("6379") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("6379") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("6379") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullTimeout() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("6379") + .timeout(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("6379") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidPatternSingle() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("6379") + .pattern("1") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidPatternSentinel() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("6379") + .pattern("2") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidPatternCluster() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("6379") + .pattern("3") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPattern() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("6379") + .pattern("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullPattern() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("6379") + .pattern(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPattern() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("6379") + .pattern("4") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPatternNonNumeric() { + RedisProtocol protocol = RedisProtocol.builder() + .host("192.168.1.1") + .port("6379") + .pattern("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/RegistryProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/RegistryProtocolTest.java new file mode 100644 index 0000000..f11e2ba --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/RegistryProtocolTest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +package org.apache.hertzbeat.common.entity.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RegistryProtocolTest { + + @Test + void isInvalid() { + + RegistryProtocol protocol1 = new RegistryProtocol(); + protocol1.setPort("8080"); + protocol1.setHost("127.0.0.1"); + assertTrue(protocol1.isInvalid()); + + RegistryProtocol protocol2 = new RegistryProtocol(); + protocol2.setPort("8080"); + protocol2.setHost("www.baidu.com"); + assertTrue(protocol2.isInvalid()); + + RegistryProtocol protocol3 = new RegistryProtocol(); + protocol3.setPort("8080"); + protocol3.setHost("www.baidu.com."); + assertFalse(protocol3.isInvalid()); + + RegistryProtocol protocol4 = new RegistryProtocol(); + protocol3.setPort("80800"); + protocol3.setHost("10.45.56.344"); + assertFalse(protocol4.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/RocketmqProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/RocketmqProtocolTest.java new file mode 100644 index 0000000..2930f7c --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/RocketmqProtocolTest.java @@ -0,0 +1,148 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RocketmqProtocolTest { + + @Test + void isInvalidValidProtocol() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost("192.168.1.1") + .namesrvPort("9876") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost("rocketmq.example.com") + .namesrvPort("9876") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost("localhost") + .namesrvPort("9876") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost("") + .namesrvPort("9876") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost(null) + .namesrvPort("9876") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost("192.168.1.1") + .namesrvPort("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost("192.168.1.1") + .namesrvPort("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost("192.168.1.1") + .namesrvPort(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidWithAccessKey() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost("192.168.1.1") + .namesrvPort("9876") + .accessKey("testAccessKey") + .secretKey("testSecretKey") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidOnlyAccessKey() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost("192.168.1.1") + .namesrvPort("9876") + .accessKey("testAccessKey") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidOnlySecretKey() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost("192.168.1.1") + .namesrvPort("9876") + .secretKey("testSecretKey") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNoAuth() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost("192.168.1.1") + .namesrvPort("9876") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidWithParseScript() { + RocketmqProtocol protocol = RocketmqProtocol.builder() + .namesrvHost("192.168.1.1") + .namesrvPort("9876") + .parseScript("$.data") + .build(); + assertFalse(protocol.isInvalid()); + } +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/S7ProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/S7ProtocolTest.java new file mode 100644 index 0000000..9d82e84 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/S7ProtocolTest.java @@ -0,0 +1,211 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class S7ProtocolTest { + + @Test + void isInvalidValidProtocol() { + S7Protocol protocol = S7Protocol.builder() + .host("192.168.1.1") + .port("102") + .driverName("s7") + .addressSyntax("S7") + .rackId("0") + .slotId("1") + .controllerType("S7_1200") + .registerAddresses(Arrays.asList("DB1.DBW0", "DB1.DBW2")) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithTimeout() { + S7Protocol protocol = S7Protocol.builder() + .host("localhost") + .port("102") + .driverName("s7") + .addressSyntax("S7") + .rackId("0") + .slotId("1") + .controllerType("S7_1200") + .timeout("5000") + .registerAddresses(Arrays.asList("DB1.DBW0")) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + S7Protocol protocol = S7Protocol.builder() + .host("") + .port("102") + .driverName("s7") + .addressSyntax("S7") + .rackId("0") + .slotId("1") + .controllerType("S7_1200") + .registerAddresses(Arrays.asList("DB1.DBW0")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + S7Protocol protocol = S7Protocol.builder() + .host("192.168.1.1") + .port("99999") + .driverName("s7") + .addressSyntax("S7") + .rackId("0") + .slotId("1") + .controllerType("S7_1200") + .registerAddresses(Arrays.asList("DB1.DBW0")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankDriverName() { + S7Protocol protocol = S7Protocol.builder() + .host("192.168.1.1") + .port("102") + .driverName("") + .addressSyntax("S7") + .rackId("0") + .slotId("1") + .controllerType("S7_1200") + .registerAddresses(Arrays.asList("DB1.DBW0")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankAddressSyntax() { + S7Protocol protocol = S7Protocol.builder() + .host("192.168.1.1") + .port("102") + .driverName("s7") + .addressSyntax("") + .rackId("0") + .slotId("1") + .controllerType("S7_1200") + .registerAddresses(Arrays.asList("DB1.DBW0")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankControllerType() { + S7Protocol protocol = S7Protocol.builder() + .host("192.168.1.1") + .port("102") + .driverName("s7") + .addressSyntax("S7") + .rackId("0") + .slotId("1") + .controllerType("") + .registerAddresses(Arrays.asList("DB1.DBW0")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNonNumericRackId() { + S7Protocol protocol = S7Protocol.builder() + .host("192.168.1.1") + .port("102") + .driverName("s7") + .addressSyntax("S7") + .rackId("abc") + .slotId("1") + .controllerType("S7_1200") + .registerAddresses(Arrays.asList("DB1.DBW0")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNonNumericSlotId() { + S7Protocol protocol = S7Protocol.builder() + .host("192.168.1.1") + .port("102") + .driverName("s7") + .addressSyntax("S7") + .rackId("0") + .slotId("xyz") + .controllerType("S7_1200") + .registerAddresses(Arrays.asList("DB1.DBW0")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNonNumericTimeout() { + S7Protocol protocol = S7Protocol.builder() + .host("192.168.1.1") + .port("102") + .driverName("s7") + .addressSyntax("S7") + .rackId("0") + .slotId("1") + .controllerType("S7_1200") + .timeout("abc") + .registerAddresses(Arrays.asList("DB1.DBW0")) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidEmptyRegisterAddresses() { + S7Protocol protocol = S7Protocol.builder() + .host("192.168.1.1") + .port("102") + .driverName("s7") + .addressSyntax("S7") + .rackId("0") + .slotId("1") + .controllerType("S7_1200") + .registerAddresses(Collections.emptyList()) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullRegisterAddresses() { + S7Protocol protocol = S7Protocol.builder() + .host("192.168.1.1") + .port("102") + .driverName("s7") + .addressSyntax("S7") + .rackId("0") + .slotId("1") + .controllerType("S7_1200") + .registerAddresses(null) + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/ScriptProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/ScriptProtocolTest.java new file mode 100644 index 0000000..3bf8dd3 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/ScriptProtocolTest.java @@ -0,0 +1,91 @@ +/* + * 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.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ScriptProtocolTest { + + @Test + void isInvalidValidScriptCommand() { + ScriptProtocol protocol = ScriptProtocol.builder() + .charset("UTF-8") + .parseType("oneRow") + .scriptTool("bash") + .scriptCommand("echo test") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidScriptPath() { + ScriptProtocol protocol = ScriptProtocol.builder() + .charset("UTF-8") + .parseType("multiRow") + .scriptTool("powershell") + .scriptPath("/tmp/test.ps1") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidUnsupportedCharset() { + ScriptProtocol protocol = ScriptProtocol.builder() + .charset("not-a-charset") + .parseType("oneRow") + .scriptTool("bash") + .scriptCommand("echo test") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUnsupportedParseType() { + ScriptProtocol protocol = ScriptProtocol.builder() + .charset("UTF-8") + .parseType("json") + .scriptTool("bash") + .scriptCommand("echo test") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUnsupportedScriptTool() { + ScriptProtocol protocol = ScriptProtocol.builder() + .charset("UTF-8") + .parseType("oneRow") + .scriptTool("sh") + .scriptCommand("echo test") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidMissingCommandAndPath() { + ScriptProtocol protocol = ScriptProtocol.builder() + .charset("UTF-8") + .parseType("oneRow") + .scriptTool("bash") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/SmtpProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/SmtpProtocolTest.java new file mode 100644 index 0000000..e47b3fe --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/SmtpProtocolTest.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class SmtpProtocolTest { + + @Test + void isInvalidValidProtocol() { + SmtpProtocol protocol = SmtpProtocol.builder() + .host("smtp.example.com") + .port("25") + .email("test@example.com") + .timeout("3000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithoutTimeout() { + SmtpProtocol protocol = SmtpProtocol.builder() + .host("smtp.example.com") + .port("25") + .email("test@example.com") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankEmail() { + SmtpProtocol protocol = SmtpProtocol.builder() + .host("smtp.example.com") + .port("25") + .email("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + SmtpProtocol protocol = SmtpProtocol.builder() + .host("") + .port("25") + .email("test@example.com") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + SmtpProtocol protocol = SmtpProtocol.builder() + .host("smtp.example.com") + .port("70000") + .email("test@example.com") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + SmtpProtocol protocol = SmtpProtocol.builder() + .host("smtp.example.com") + .port("25") + .email("test@example.com") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/SnmpProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/SnmpProtocolTest.java new file mode 100644 index 0000000..34fc995 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/SnmpProtocolTest.java @@ -0,0 +1,144 @@ +/* + * 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.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class SnmpProtocolTest { + + @Test + void isInvalidValidV2cProtocol() { + SnmpProtocol protocol = SnmpProtocol.builder() + .host("192.168.1.1") + .port("161") + .version("2c") + .community("public") + .oids(validOids()) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidNumericV1Protocol() { + SnmpProtocol protocol = SnmpProtocol.builder() + .host("192.168.1.1") + .port("161") + .version("0") + .community("public") + .oids(validOids()) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidNumericV2cProtocol() { + SnmpProtocol protocol = SnmpProtocol.builder() + .host("192.168.1.1") + .port("161") + .version("1") + .community("public") + .oids(validOids()) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidV3Protocol() { + SnmpProtocol protocol = SnmpProtocol.builder() + .host("192.168.1.1") + .port("161") + .version("3") + .username("user") + .authPassphrase("authPass") + .privPassphrase("privPass") + .oids(validOids()) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidMissingCommunityForV2c() { + SnmpProtocol protocol = SnmpProtocol.builder() + .host("192.168.1.1") + .port("161") + .version("2c") + .oids(validOids()) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidMissingCredentialsForV3() { + SnmpProtocol protocol = SnmpProtocol.builder() + .host("192.168.1.1") + .port("161") + .version("v3") + .username("user") + .oids(validOids()) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUnsupportedOperation() { + SnmpProtocol protocol = SnmpProtocol.builder() + .host("192.168.1.1") + .port("161") + .version("2c") + .community("public") + .operation("bulk") + .oids(validOids()) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidEmptyOids() { + SnmpProtocol protocol = SnmpProtocol.builder() + .host("192.168.1.1") + .port("161") + .version("2c") + .community("public") + .oids(Map.of()) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUnsupportedVersion() { + SnmpProtocol protocol = SnmpProtocol.builder() + .host("192.168.1.1") + .port("161") + .version("4") + .community("public") + .oids(validOids()) + .build(); + assertTrue(protocol.isInvalid()); + } + + private Map<String, String> validOids() { + Map<String, String> oids = new HashMap<>(); + oids.put("name", "1.3.6.1.2.1.1.5.0"); + return oids; + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/SshProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/SshProtocolTest.java new file mode 100644 index 0000000..ca0577e --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/SshProtocolTest.java @@ -0,0 +1,243 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SshProtocolTest { + + @Test + void isInvalidValidProtocol() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + SshProtocol protocol = SshProtocol.builder() + .host("ssh.example.com") + .port("22") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + SshProtocol protocol = SshProtocol.builder() + .host("localhost") + .port("22") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithIpv6() { + SshProtocol protocol = SshProtocol.builder() + .host("::1") + .port("22") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + SshProtocol protocol = SshProtocol.builder() + .host("") + .port("22") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + SshProtocol protocol = SshProtocol.builder() + .host(null) + .port("22") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullTimeout() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .timeout(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidReuseConnection() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .reuseConnection("true") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidReuseConnectionFalse() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .reuseConnection("false") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidReuseConnection() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .reuseConnection("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidUseProxyFalse() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .useProxy("false") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidUseProxyTrue() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .useProxy("true") + .proxyHost("proxy.example.com") + .proxyPort("1080") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidUseProxyValue() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .useProxy("invalid") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUseProxyTrueWithoutProxyHost() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .useProxy("true") + .proxyPort("1080") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUseProxyTrueWithoutProxyPort() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .useProxy("true") + .proxyHost("proxy.example.com") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidUseProxyTrueWithInvalidProxyPort() { + SshProtocol protocol = SshProtocol.builder() + .host("192.168.1.1") + .port("22") + .useProxy("true") + .proxyHost("proxy.example.com") + .proxyPort("99999") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/TelnetProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/TelnetProtocolTest.java new file mode 100644 index 0000000..8892746 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/TelnetProtocolTest.java @@ -0,0 +1,177 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TelnetProtocolTest { + + @Test + void isInvalidValidProtocol() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("192.168.1.1") + .port("23") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("telnet.example.com") + .port("23") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("localhost") + .port("23") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithIpv6() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("::1") + .port("23") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("") + .port("23") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host(null) + .port("23") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("192.168.1.1") + .port("23") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("192.168.1.1") + .port("23") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullTimeout() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("192.168.1.1") + .port("23") + .timeout(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("192.168.1.1") + .port("23") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidCmd() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("192.168.1.1") + .port("23") + .cmd("ls -la") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankCmd() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("192.168.1.1") + .port("23") + .cmd("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullCmd() { + TelnetProtocol protocol = TelnetProtocol.builder() + .host("192.168.1.1") + .port("23") + .cmd(null) + .build(); + assertFalse(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/UdpProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/UdpProtocolTest.java new file mode 100644 index 0000000..b96f0cf --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/UdpProtocolTest.java @@ -0,0 +1,207 @@ +/* + * 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.job.protocol; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class UdpProtocolTest { + + @Test + void isInvalidValidProtocol() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("53") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithDomain() { + UdpProtocol protocol = UdpProtocol.builder() + .host("dns.example.com") + .port("53") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithLocalhost() { + UdpProtocol protocol = UdpProtocol.builder() + .host("localhost") + .port("53") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithIpv6() { + UdpProtocol protocol = UdpProtocol.builder() + .host("::1") + .port("53") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + UdpProtocol protocol = UdpProtocol.builder() + .host("") + .port("53") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullHost() { + UdpProtocol protocol = UdpProtocol.builder() + .host(null) + .port("53") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("99999") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPort() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidNullPort() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port(null) + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidTimeout() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("53") + .timeout("5000") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankTimeout() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("53") + .timeout("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullTimeout() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("53") + .timeout(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidTimeout() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("53") + .timeout("abc") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidValidContent() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("53") + .content("48454C4C4F") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidContentLowercase() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("53") + .content("48454c4c4f") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankContent() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("53") + .content("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidNullContent() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("53") + .content(null) + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidContent() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("53") + .content("GGGG") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidContentWithSpecialChar() { + UdpProtocol protocol = UdpProtocol.builder() + .host("192.168.1.1") + .port("53") + .content("48454C4C4F!") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/WebsocketProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/WebsocketProtocolTest.java new file mode 100644 index 0000000..dad6650 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/WebsocketProtocolTest.java @@ -0,0 +1,96 @@ +/* + * 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.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class WebsocketProtocolTest { + + @Test + void isInvalidValidProtocol() { + WebsocketProtocol protocol = WebsocketProtocol.builder() + .host("127.0.0.1") + .port("80") + .path("/") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidValidProtocolWithoutPath() { + WebsocketProtocol protocol = WebsocketProtocol.builder() + .host("127.0.0.1") + .port("80") + .path("") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidHost() { + WebsocketProtocol protocol = WebsocketProtocol.builder() + .host("") + .port("80") + .path("/") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidInvalidPort() { + WebsocketProtocol protocol = WebsocketProtocol.builder() + .host("127.0.0.1") + .port("99999") + .path("/") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidZeroPort() { + WebsocketProtocol protocol = WebsocketProtocol.builder() + .host("127.0.0.1") + .port("0") + .path("/") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidPathWithoutLeadingSlash() { + WebsocketProtocol protocol = WebsocketProtocol.builder() + .host("127.0.0.1") + .port("80") + .path("ws") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidPathContainsWhitespace() { + WebsocketProtocol protocol = WebsocketProtocol.builder() + .host("127.0.0.1") + .port("80") + .path("/chat room") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/ZookeeperSdProtocolTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/ZookeeperSdProtocolTest.java new file mode 100644 index 0000000..1c10ec7 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/protocol/ZookeeperSdProtocolTest.java @@ -0,0 +1,71 @@ +/* + * 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.job.protocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ZookeeperSdProtocolTest { + + @Test + void isInvalidValidProtocol() { + ZookeeperSdProtocol protocol = ZookeeperSdProtocol.builder() + .url("zk1:2181,zk2:2181") + .pathPrefix("/services") + .build(); + assertFalse(protocol.isInvalid()); + } + + @Test + void isInvalidBlankUrl() { + ZookeeperSdProtocol protocol = ZookeeperSdProtocol.builder() + .url("") + .pathPrefix("/services") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidBlankPathPrefix() { + ZookeeperSdProtocol protocol = ZookeeperSdProtocol.builder() + .url("localhost:2181") + .pathPrefix("") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidPathPrefixWithoutSlash() { + ZookeeperSdProtocol protocol = ZookeeperSdProtocol.builder() + .url("localhost:2181") + .pathPrefix("services") + .build(); + assertTrue(protocol.isInvalid()); + } + + @Test + void isInvalidPathPrefixWithWhitespace() { + ZookeeperSdProtocol protocol = ZookeeperSdProtocol.builder() + .url("localhost:2181") + .pathPrefix("/service path") + .build(); + assertTrue(protocol.isInvalid()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/message/CollectRepTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/message/CollectRepTest.java new file mode 100644 index 0000000..2cfcb51 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/message/CollectRepTest.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.common.entity.message; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test case for {@link CollectRep} + */ +public class CollectRepTest { + + @ParameterizedTest + @CsvSource(value = { + "name, name, true", + "name1, name3, false", + }) + void testFieldEquals(String name1, String name2, boolean result) { + CollectRep.Field field1 = new CollectRep.Field(); + field1.setName(name1); + CollectRep.Field field2 = new CollectRep.Field(); + field2.setName(name2); + + assertEquals(field1.equals(field2), result); + } + +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/plugin/PluginContextTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/plugin/PluginContextTest.java new file mode 100644 index 0000000..a434e40 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/plugin/PluginContextTest.java @@ -0,0 +1,100 @@ +/* + * 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.plugin; + + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +/** + * Test case for {@link PluginContext} + */ +class PluginContextTest { + + PluginContext pluginContext; + Configmap host = new Configmap("host", "127.0.0.1", (byte) 1); + + @BeforeEach + void init() { + + List<Configmap> params = Arrays.asList(host, + new Configmap("port", "80", (byte) 0), + new Configmap("enableSsl", "true", (byte) 1), + new Configmap("max", "1725116706000", (byte) 1) + ); + pluginContext = PluginContext.builder().params(params).build(); + } + + @Test + void getString() { + String host = pluginContext.param().getString("host", "192.168.0.1"); + assertEquals("127.0.0.1", host); + } + + @Test + void getInteger() { + Integer port = pluginContext.param().getInteger("port", 100); + assertEquals(80, port); + } + + @Test + void testGetNonExistentParameter() { + Integer num = pluginContext.param().getInteger("num", 100); + assertEquals(100, num); + } + + @Test + void testErrorFormat() { + Integer host = pluginContext.param().getInteger("host", 100); + assertEquals(100, host); + } + + @Test + void getBoolean() { + boolean enableSsl = pluginContext.param().getBoolean("enableSsl", false); + assertTrue(enableSsl); + } + + @Test + void getLong() { + Long max = pluginContext.param().getLong("max", 1800000000000L); + assertEquals(1725116706000L, max); + } + + @Test + void testImmutableData() { + List<Configmap> list = pluginContext.param().allParams(); + Optional<Configmap> param = list.stream().filter(v -> v.getKey().equals("host")).findAny(); + if (param.isEmpty()) { + throw new RuntimeException("error"); + } + param.get().setValue("192.168.1.1"); + assertEquals(host.getValue(), "127.0.0.1"); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/KafkaLogEntryDeserializerTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/KafkaLogEntryDeserializerTest.java new file mode 100644 index 0000000..87484a3 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/KafkaLogEntryDeserializerTest.java @@ -0,0 +1,267 @@ +/* + * 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.serialize; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.kafka.common.header.Headers; +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; + +/** + * Test case for {@link KafkaLogEntryDeserializer} + */ +@ExtendWith(MockitoExtension.class) +class KafkaLogEntryDeserializerTest { + + private KafkaLogEntryDeserializer deserializer; + private KafkaLogEntrySerializer serializer; + + @Mock + private Map<String, ?> configs; + + @Mock + private Headers headers; + + @BeforeEach + void setUp() { + deserializer = new KafkaLogEntryDeserializer(); + serializer = new KafkaLogEntrySerializer(); + } + + @Test + void testConfigure() { + // Test that configure method doesn't throw any exceptions + deserializer.configure(configs, false); + deserializer.configure(configs, true); + } + + @Test + void testDeserializeWithValidLogEntry() { + // Create a comprehensive log entry + Map<String, Object> attributes = new HashMap<>(); + attributes.put("service.name", "hertzbeat"); + attributes.put("service.version", "1.0.0"); + attributes.put("environment", "test"); + + Map<String, Object> resource = new HashMap<>(); + resource.put("host.name", "localhost"); + resource.put("os.type", "linux"); + + LogEntry.InstrumentationScope scope = LogEntry.InstrumentationScope.builder() + .name("org.apache.hertzbeat.test") + .version("1.0.0") + .attributes(new HashMap<>()) + .droppedAttributesCount(0) + .build(); + + LogEntry originalLogEntry = LogEntry.builder() + .timeUnixNano(1234567890123456789L) + .observedTimeUnixNano(1234567890123456790L) + .severityNumber(9) // INFO level + .severityText("INFO") + .body("Test log message for deserialization") + .attributes(attributes) + .droppedAttributesCount(0) + .traceId("1234567890abcdef1234567890abcdef") + .spanId("1234567890abcdef") + .traceFlags(1) + .resource(resource) + .instrumentationScope(scope) + .build(); + + // Serialize the log entry to bytes + byte[] bytes = serializer.serialize("test-topic", originalLogEntry); + assertNotNull(bytes); + + // Deserialize the bytes back to log entry + LogEntry deserializedLogEntry = deserializer.deserialize("test-topic", bytes); + + assertNotNull(deserializedLogEntry); + assertEquals(originalLogEntry.getTimeUnixNano(), deserializedLogEntry.getTimeUnixNano()); + assertEquals(originalLogEntry.getObservedTimeUnixNano(), deserializedLogEntry.getObservedTimeUnixNano()); + assertEquals(originalLogEntry.getSeverityNumber(), deserializedLogEntry.getSeverityNumber()); + assertEquals(originalLogEntry.getSeverityText(), deserializedLogEntry.getSeverityText()); + assertEquals(originalLogEntry.getBody(), deserializedLogEntry.getBody()); + assertEquals(originalLogEntry.getTraceId(), deserializedLogEntry.getTraceId()); + assertEquals(originalLogEntry.getSpanId(), deserializedLogEntry.getSpanId()); + assertEquals(originalLogEntry.getTraceFlags(), deserializedLogEntry.getTraceFlags()); + assertEquals(originalLogEntry.getDroppedAttributesCount(), deserializedLogEntry.getDroppedAttributesCount()); + + // Verify attributes + assertNotNull(deserializedLogEntry.getAttributes()); + assertEquals("hertzbeat", deserializedLogEntry.getAttributes().get("service.name")); + assertEquals("1.0.0", deserializedLogEntry.getAttributes().get("service.version")); + assertEquals("test", deserializedLogEntry.getAttributes().get("environment")); + + // Verify resource + assertNotNull(deserializedLogEntry.getResource()); + assertEquals("localhost", deserializedLogEntry.getResource().get("host.name")); + assertEquals("linux", deserializedLogEntry.getResource().get("os.type")); + + // Verify instrumentation scope + assertNotNull(deserializedLogEntry.getInstrumentationScope()); + assertEquals("org.apache.hertzbeat.test", deserializedLogEntry.getInstrumentationScope().getName()); + assertEquals("1.0.0", deserializedLogEntry.getInstrumentationScope().getVersion()); + } + + @Test + void testDeserializeWithSimpleLogEntry() { + // Test with minimal log entry + LogEntry originalLogEntry = LogEntry.builder() + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .severityNumber(17) // ERROR level + .severityText("ERROR") + .body("Simple error message") + .build(); + + // Serialize and deserialize + byte[] bytes = serializer.serialize("test-topic", originalLogEntry); + LogEntry deserializedLogEntry = deserializer.deserialize("test-topic", bytes); + + assertNotNull(deserializedLogEntry); + assertEquals(originalLogEntry.getTimeUnixNano(), deserializedLogEntry.getTimeUnixNano()); + assertEquals(originalLogEntry.getSeverityNumber(), deserializedLogEntry.getSeverityNumber()); + assertEquals(originalLogEntry.getSeverityText(), deserializedLogEntry.getSeverityText()); + assertEquals(originalLogEntry.getBody(), deserializedLogEntry.getBody()); + } + + @Test + void testDeserializeWithNullData() { + // Test deserialization with null data + LogEntry logEntry = deserializer.deserialize("test-topic", null); + + assertNull(logEntry); + } + + @Test + void testDeserializeWithEmptyData() { + // Test deserialization with empty data + byte[] emptyData = new byte[0]; + LogEntry logEntry = deserializer.deserialize("test-topic", emptyData); + + assertNull(logEntry); + } + + @Test + void testDeserializeWithInvalidData() { + // Test deserialization with invalid JSON data + byte[] invalidData = "invalid json data".getBytes(StandardCharsets.UTF_8); + LogEntry logEntry = deserializer.deserialize("test-topic", invalidData); + + assertNull(logEntry); + } + + @Test + void testDeserializeWithMalformedJson() { + // Test deserialization with malformed JSON + String malformedJson = "{\"severityText\":\"INFO\",\"body\":\"test\","; // Missing closing brace + byte[] malformedData = malformedJson.getBytes(StandardCharsets.UTF_8); + LogEntry logEntry = deserializer.deserialize("test-topic", malformedData); + + assertNull(logEntry); + } + + @Test + void testDeserializeWithHeaders() { + // Test deserialization with headers + LogEntry originalLogEntry = LogEntry.builder() + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .severityNumber(13) // WARN level + .severityText("WARN") + .body("Warning message with headers") + .build(); + + // Serialize and deserialize with headers + byte[] bytes = serializer.serialize("test-topic", headers, originalLogEntry); + LogEntry deserializedLogEntry = deserializer.deserialize("test-topic", headers, bytes); + + assertNotNull(deserializedLogEntry); + assertEquals(originalLogEntry.getSeverityText(), deserializedLogEntry.getSeverityText()); + assertEquals(originalLogEntry.getBody(), deserializedLogEntry.getBody()); + assertEquals(originalLogEntry.getSeverityNumber(), deserializedLogEntry.getSeverityNumber()); + } + + @Test + void testDeserializeWithComplexAttributes() { + // Test with complex nested attributes + Map<String, Object> attributes = new HashMap<>(); + attributes.put("string_value", "test_string"); + attributes.put("numeric_value", 42); + attributes.put("boolean_value", true); + + // Note: Nested objects will be deserialized as LinkedHashMap by Jackson + Map<String, Object> nestedMap = new HashMap<>(); + nestedMap.put("nested_key", "nested_value"); + attributes.put("nested_object", nestedMap); + + LogEntry originalLogEntry = LogEntry.builder() + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .severityNumber(5) // DEBUG level + .severityText("DEBUG") + .body("Debug message with complex attributes") + .attributes(attributes) + .build(); + + // Serialize and deserialize + byte[] bytes = serializer.serialize("test-topic", originalLogEntry); + LogEntry deserializedLogEntry = deserializer.deserialize("test-topic", bytes); + + assertNotNull(deserializedLogEntry); + assertEquals(originalLogEntry.getSeverityText(), deserializedLogEntry.getSeverityText()); + assertEquals(originalLogEntry.getBody(), deserializedLogEntry.getBody()); + + // Verify complex attributes + assertNotNull(deserializedLogEntry.getAttributes()); + assertEquals("test_string", deserializedLogEntry.getAttributes().get("string_value")); + assertEquals(42, deserializedLogEntry.getAttributes().get("numeric_value")); + assertEquals(true, deserializedLogEntry.getAttributes().get("boolean_value")); + + // Verify nested object (will be LinkedHashMap after JSON deserialization) + Object nestedObject = deserializedLogEntry.getAttributes().get("nested_object"); + assertNotNull(nestedObject); + } + + @Test + void testDeserializeWithPartialLogEntry() { + // Test with valid JSON but partial log entry structure + String partialJson = "{\"severityText\":\"INFO\",\"body\":\"partial log entry\"}"; + byte[] partialData = partialJson.getBytes(StandardCharsets.UTF_8); + LogEntry logEntry = deserializer.deserialize("test-topic", partialData); + + assertNotNull(logEntry); + assertEquals("INFO", logEntry.getSeverityText()); + assertEquals("partial log entry", logEntry.getBody()); + assertNull(logEntry.getSeverityNumber()); // Not provided in JSON + assertNull(logEntry.getTimeUnixNano()); // Not provided in JSON + } + + @Test + void testClose() { + // Test that close method doesn't throw any exceptions + deserializer.close(); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/KafkaLogEntrySerializerTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/KafkaLogEntrySerializerTest.java new file mode 100644 index 0000000..55a4dd6 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/KafkaLogEntrySerializerTest.java @@ -0,0 +1,202 @@ +/* + * 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.serialize; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.kafka.common.header.Headers; +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; + +/** + * Test case for {@link KafkaLogEntrySerializer} + */ +@ExtendWith(MockitoExtension.class) +class KafkaLogEntrySerializerTest { + + private KafkaLogEntrySerializer serializer; + + @Mock + private Map<String, ?> configs; + + @Mock + private Headers headers; + + @BeforeEach + void setUp() { + serializer = new KafkaLogEntrySerializer(); + } + + @Test + void testConfigure() { + // Test that configure method doesn't throw any exceptions + serializer.configure(configs, false); + serializer.configure(configs, true); + } + + @Test + void testSerializeWithLogEntry() { + // Create a comprehensive log entry for testing + Map<String, Object> attributes = new HashMap<>(); + attributes.put("service.name", "hertzbeat"); + attributes.put("service.version", "1.0.0"); + attributes.put("environment", "test"); + + Map<String, Object> resource = new HashMap<>(); + resource.put("host.name", "localhost"); + resource.put("os.type", "linux"); + + LogEntry.InstrumentationScope scope = LogEntry.InstrumentationScope.builder() + .name("org.apache.hertzbeat.test") + .version("1.0.0") + .attributes(new HashMap<>()) + .droppedAttributesCount(0) + .build(); + + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .observedTimeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .severityNumber(9) // INFO level + .severityText("INFO") + .body("Test log message for serialization") + .attributes(attributes) + .droppedAttributesCount(0) + .traceId("1234567890abcdef1234567890abcdef") + .spanId("1234567890abcdef") + .traceFlags(1) + .resource(resource) + .instrumentationScope(scope) + .build(); + + byte[] bytes = serializer.serialize("test-topic", logEntry); + + assertNotNull(bytes); + assertTrue(bytes.length > 0); + + // Verify the JSON contains expected fields + String jsonString = new String(bytes, StandardCharsets.UTF_8); + assertTrue(jsonString.contains("\"severityText\":\"INFO\"")); + assertTrue(jsonString.contains("\"body\":\"Test log message for serialization\"")); + assertTrue(jsonString.contains("\"traceId\":\"1234567890abcdef1234567890abcdef\"")); + assertTrue(jsonString.contains("\"spanId\":\"1234567890abcdef\"")); + } + + @Test + void testSerializeWithSimpleLogEntry() { + // Test with minimal log entry + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .severityNumber(17) // ERROR level + .severityText("ERROR") + .body("Simple error message") + .build(); + + byte[] bytes = serializer.serialize("test-topic", logEntry); + + assertNotNull(bytes); + assertTrue(bytes.length > 0); + + String jsonString = new String(bytes, StandardCharsets.UTF_8); + assertTrue(jsonString.contains("\"severityText\":\"ERROR\"")); + assertTrue(jsonString.contains("\"body\":\"Simple error message\"")); + assertTrue(jsonString.contains("\"severityNumber\":17")); + } + + @Test + void testSerializeWithNullLogEntry() { + // Test serialization with null log entry + byte[] bytes = serializer.serialize("test-topic", null); + + assertNull(bytes); + } + + @Test + void testSerializeWithHeaders() { + // Test serialization with headers + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .severityNumber(13) // WARN level + .severityText("WARN") + .body("Warning message with headers") + .build(); + + byte[] bytes = serializer.serialize("test-topic", headers, logEntry); + + assertNotNull(bytes); + assertTrue(bytes.length > 0); + + String jsonString = new String(bytes, StandardCharsets.UTF_8); + assertTrue(jsonString.contains("\"severityText\":\"WARN\"")); + assertTrue(jsonString.contains("\"body\":\"Warning message with headers\"")); + } + + @Test + void testSerializeWithNullLogEntryAndHeaders() { + // Test serialization with null log entry and headers + byte[] bytes = serializer.serialize("test-topic", headers, null); + + assertNull(bytes); + } + + @Test + void testSerializeWithComplexAttributes() { + // Test with complex nested attributes + Map<String, Object> attributes = new HashMap<>(); + attributes.put("string_value", "test_string"); + attributes.put("numeric_value", 42); + attributes.put("boolean_value", true); + + Map<String, Object> nestedMap = new HashMap<>(); + nestedMap.put("nested_key", "nested_value"); + attributes.put("nested_object", nestedMap); + + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .severityNumber(5) // DEBUG level + .severityText("DEBUG") + .body("Debug message with complex attributes") + .attributes(attributes) + .build(); + + byte[] bytes = serializer.serialize("test-topic", logEntry); + + assertNotNull(bytes); + assertTrue(bytes.length > 0); + + String jsonString = new String(bytes, StandardCharsets.UTF_8); + assertTrue(jsonString.contains("\"severityText\":\"DEBUG\"")); + assertTrue(jsonString.contains("\"string_value\":\"test_string\"")); + assertTrue(jsonString.contains("\"numeric_value\":42")); + assertTrue(jsonString.contains("\"boolean_value\":true")); + } + + @Test + void testClose() { + // Test that close method doesn't throw any exceptions + serializer.close(); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/KafkaMetricsDataDeserializerTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/KafkaMetricsDataDeserializerTest.java new file mode 100644 index 0000000..a5fa00b --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/KafkaMetricsDataDeserializerTest.java @@ -0,0 +1,124 @@ +/* + * 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.serialize; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.ipc.ArrowStreamWriter; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.kafka.common.header.Headers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * test case for {@link KafkaMetricsDataDeserializer} + */ + +class KafkaMetricsDataDeserializerTest { + + private KafkaMetricsDataDeserializer deserializer; + + @Mock + private Map<String, ?> configs; + + @Mock + private Headers headers; + + @BeforeEach + void setUp() { + + MockitoAnnotations.openMocks(this); + + deserializer = new KafkaMetricsDataDeserializer(); + } + + @Test + void testConfigure() { + + deserializer.configure(configs, false); + } + + @Test + void testDeserializeWithBytes() { + + CollectRep.MetricsData expectedMetricsData = CollectRep.MetricsData.newBuilder() + .setMetrics("someValue") + .setApp("linux") + .build(); + byte[] bytes = null; + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + VectorSchemaRoot root = expectedMetricsData.toVectorSchemaRootAndRelease(); + ArrowStreamWriter writer = new ArrowStreamWriter(root, + null, Channels.newChannel(out))) { + writer.start(); + writer.writeBatch(); + writer.end(); + bytes = out.toByteArray(); + } catch (IOException ignored) {} + + CollectRep.MetricsData actualMetricsData = deserializer.deserialize("", bytes); + + assertEquals(expectedMetricsData.rowCount(), actualMetricsData.rowCount()); + } + + @Test + void testDeserializeWithInvalidBytes() { + + byte[] invalidBytes = "invalid data".getBytes(StandardCharsets.UTF_8); + + assertThrows(RuntimeException.class, () -> deserializer.deserialize("", invalidBytes)); + } + + @Test + void testDeserializeWithHeaders() { + + CollectRep.MetricsData expectedMetricsData = CollectRep.MetricsData.newBuilder() + .setMetrics("someValue") + .setApp("linux") + .build(); + byte[] bytes = null; + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + VectorSchemaRoot root = expectedMetricsData.toVectorSchemaRootAndRelease(); + ArrowStreamWriter writer = new ArrowStreamWriter(root, + null, Channels.newChannel(out))) { + writer.start(); + writer.writeBatch(); + writer.end(); + bytes = out.toByteArray(); + } catch (IOException ignored) {} + + CollectRep.MetricsData actualMetricsData = deserializer.deserialize("topic", headers, bytes); + + assertEquals(expectedMetricsData.rowCount(), actualMetricsData.rowCount()); + } + + @Test + void testClose() { + + deserializer.close(); + } + +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/KafkaMetricsDataSerializerTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/KafkaMetricsDataSerializerTest.java new file mode 100644 index 0000000..dbb7807 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/KafkaMetricsDataSerializerTest.java @@ -0,0 +1,111 @@ +/* + * 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.serialize; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import java.util.Map; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.ipc.ArrowStreamWriter; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.kafka.common.header.Headers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * test case for {@link KafkaMetricsDataSerializer} + */ + +class KafkaMetricsDataSerializerTest { + + private KafkaMetricsDataSerializer serializer; + + @Mock + private Map<String, ?> configs; + + @Mock + private Headers headers; + + @BeforeEach + void setUp() { + + MockitoAnnotations.openMocks(this); + + serializer = new KafkaMetricsDataSerializer(); + } + + @Test + void testConfigure() { + + serializer.configure(configs, false); + } + + @Test + void testSerializeWithMetricsData() { + + CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() + .setMetrics("someValue") + .setApp("linux") + .build(); + byte[] bytes = serializer.serialize("", metricsData); + + assertNotNull(bytes); + } + + @Test + void testSerializeWithNullMetricsData() { + + byte[] bytes = serializer.serialize("", null); + + assertNull(bytes); + } + + @Test + void testSerializeWithHeaders() { + + CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() + .setMetrics("someValue") + .setApp("linux") + .build(); + byte[] expectedBytes = null; + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + VectorSchemaRoot root = metricsData.toVectorSchemaRootAndRelease(); + ArrowStreamWriter writer = new ArrowStreamWriter(root, + null, Channels.newChannel(out))) { + writer.start(); + writer.writeBatch(); + writer.end(); + expectedBytes = out.toByteArray(); + } catch (IOException ignored) {} + byte[] bytes = serializer.serialize("topic", headers, metricsData); + assertArrayEquals(expectedBytes, bytes); + } + + @Test + void testClose() { + + serializer.close(); + } + +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/RedisLogEntryCodecTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/RedisLogEntryCodecTest.java new file mode 100644 index 0000000..de95cfc --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/serialize/RedisLogEntryCodecTest.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.serialize; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link RedisLogEntryCodec} + */ +@ExtendWith(MockitoExtension.class) +class RedisLogEntryCodecTest { + + private RedisLogEntryCodec codec; + + @BeforeEach + void setUp() { + codec = new RedisLogEntryCodec(); + } + + @Test + void testEncodeDecodeKey() { + String key = "test-log-key"; + + // Test encoding + ByteBuffer encodedKey = codec.encodeKey(key); + assertNotNull(encodedKey); + + // Test decoding + String decodedKey = codec.decodeKey(encodedKey); + assertEquals(key, decodedKey); + } + + @Test + void testEncodeDecodeSimpleLogEntry() { + // Create a simple log entry + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .severityNumber(17) // ERROR level + .severityText("ERROR") + .body("Simple error message") + .build(); + + // Test encoding + ByteBuffer encodedValue = codec.encodeValue(logEntry); + assertNotNull(encodedValue); + assertTrue(encodedValue.hasRemaining()); + + // Test decoding + LogEntry decodedLogEntry = codec.decodeValue(encodedValue); + assertNotNull(decodedLogEntry); + assertEquals(logEntry.getSeverityNumber(), decodedLogEntry.getSeverityNumber()); + assertEquals(logEntry.getSeverityText(), decodedLogEntry.getSeverityText()); + assertEquals(logEntry.getBody(), decodedLogEntry.getBody()); + assertEquals(logEntry.getTimeUnixNano(), decodedLogEntry.getTimeUnixNano()); + } + + @Test + void testEncodeDecodeComplexLogEntry() { + // Create a comprehensive log entry for testing + Map<String, Object> attributes = new HashMap<>(); + attributes.put("service.name", "hertzbeat"); + attributes.put("service.version", "1.0.0"); + attributes.put("environment", "test"); + attributes.put("numeric_value", 42); + attributes.put("boolean_value", true); + + Map<String, Object> resource = new HashMap<>(); + resource.put("host.name", "localhost"); + resource.put("os.type", "linux"); + + LogEntry.InstrumentationScope scope = LogEntry.InstrumentationScope.builder() + .name("org.apache.hertzbeat.test") + .version("1.0.0") + .attributes(new HashMap<>()) + .droppedAttributesCount(0) + .build(); + + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .observedTimeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .severityNumber(9) // INFO level + .severityText("INFO") + .body("Test log message for Redis codec") + .attributes(attributes) + .droppedAttributesCount(0) + .traceId("1234567890abcdef1234567890abcdef") + .spanId("1234567890abcdef") + .traceFlags(1) + .resource(resource) + .instrumentationScope(scope) + .build(); + + // Test encoding + ByteBuffer encodedValue = codec.encodeValue(logEntry); + assertNotNull(encodedValue); + assertTrue(encodedValue.hasRemaining()); + + // Verify the JSON structure + String jsonString = new String(encodedValue.array(), StandardCharsets.UTF_8); + assertTrue(jsonString.contains("\"severityText\":\"INFO\"")); + assertTrue(jsonString.contains("\"body\":\"Test log message for Redis codec\"")); + assertTrue(jsonString.contains("\"traceId\":\"1234567890abcdef1234567890abcdef\"")); + assertTrue(jsonString.contains("\"spanId\":\"1234567890abcdef\"")); + + // Test decoding + LogEntry decodedLogEntry = codec.decodeValue(encodedValue); + assertNotNull(decodedLogEntry); + assertEquals(logEntry.getSeverityNumber(), decodedLogEntry.getSeverityNumber()); + assertEquals(logEntry.getSeverityText(), decodedLogEntry.getSeverityText()); + assertEquals(logEntry.getBody(), decodedLogEntry.getBody()); + assertEquals(logEntry.getTraceId(), decodedLogEntry.getTraceId()); + assertEquals(logEntry.getSpanId(), decodedLogEntry.getSpanId()); + assertEquals(logEntry.getTraceFlags(), decodedLogEntry.getTraceFlags()); + } + + @Test + void testDecodeEmptyByteBuffer() { + // Test decoding empty byte buffer + ByteBuffer emptyBuffer = ByteBuffer.allocate(0); + LogEntry decodedLogEntry = codec.decodeValue(emptyBuffer); + assertNull(decodedLogEntry); + } + + @Test + void testDecodeInvalidJson() { + // Test decoding invalid JSON + String invalidJson = "{ invalid json }"; + ByteBuffer invalidBuffer = ByteBuffer.wrap(invalidJson.getBytes(StandardCharsets.UTF_8)); + + LogEntry decodedLogEntry = codec.decodeValue(invalidBuffer); + assertNull(decodedLogEntry); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/AesUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/AesUtilTest.java new file mode 100644 index 0000000..9bff2d9 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/AesUtilTest.java @@ -0,0 +1,104 @@ +/* + * 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.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.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 org.junit.jupiter.api.Test; + +/** + * Test case for {@link AesUtil} + */ +class AesUtilTest { + + private static final String ALGORITHM_STR = "AES/CBC/PKCS5Padding"; + + private static final String AES = "AES"; + + private static final String VALID_KEY = "1234567890123456"; + + private static final String ENCODE_RULES = "defaultsecretkey"; + + @Test + void testAesEncode() { + String originalText = "This is a secret message"; + String encryptedText = aesEncode(originalText, VALID_KEY); + assertNotEquals(originalText, encryptedText); + + String decryptedText = aesDecode(encryptedText, VALID_KEY); + assertEquals(originalText, decryptedText); + } + + @Test + void testAesDecode() { + // Test with invalid key + String originalText = "This is a secret message"; + String encryptedText = aesEncode(originalText, VALID_KEY); + String decryptedText = aesDecode(encryptedText, "invalidkey123456"); + assertNotEquals(originalText, decryptedText); + + // Test with default key + originalText = "This is a secret message"; + encryptedText = aesEncode(originalText, VALID_KEY); + decryptedText = aesDecode(encryptedText, "invalidkey123456"); + if (!decryptedText.equals(originalText)) { + decryptedText = aesDecode(encryptedText, ENCODE_RULES); + } + assertNotEquals(originalText, decryptedText); + } + + @Test + void testIsCiphertext() { + + // Test with valid key + String originalText = "This is a secret message"; + String encryptedText = aesEncode(originalText, VALID_KEY); + assertTrue(isCiphertext(encryptedText, VALID_KEY)); + + // Test with plain text, normal text is not ciphertext + String plainText = "This is not encrypted"; + assertFalse(isCiphertext(plainText, VALID_KEY)); + + // Test with invalid base64 text + String invalidBase64Text = "InvalidBase64"; + assertFalse(isCiphertext(invalidBase64Text, VALID_KEY)); + + // Test with invalid key + originalText = "This is a secret message"; + encryptedText = aesEncode(originalText, VALID_KEY); + String invalidKey = "6543210987654321"; + assertFalse(isCiphertext(encryptedText, invalidKey)); + } + + @Test + void testDefaultKeyCompatibility() { + // Test with default key + String originalText = "This is a secret message"; + // encode use default secret key + String encryptedText = aesEncode(originalText, AesUtil.DEFAULT_ENCODE_RULES); + // decode use new secret key + String decryptedText = aesDecode(encryptedText, "newkey1234567890"); + // old data can decode with default secret key + assertEquals(originalText, decryptedText); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/ArrowUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/ArrowUtilTest.java new file mode 100644 index 0000000..beb0d2c --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/ArrowUtilTest.java @@ -0,0 +1,123 @@ +/* + * 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.util; + +import com.google.common.collect.Lists; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.BigIntVector; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.types.pojo.ArrowType; +import org.apache.arrow.vector.types.pojo.Field; +import org.apache.arrow.vector.types.pojo.FieldType; +import org.apache.arrow.vector.types.pojo.Schema; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Test case for {@link ArrowUtil} + */ +class ArrowUtilTest { + + @Test + void testSerializeAndDeserializeMultipleRoots() { + RootAllocator allocator = new RootAllocator(); + List<VectorSchemaRoot> roots = new ArrayList<>(); + + // Create first root + Field field1 = new Field("field1", FieldType.nullable(new ArrowType.Int(64, true)), null); + Schema schema1 = new Schema(Collections.singletonList(field1)); + VectorSchemaRoot root1 = VectorSchemaRoot.create(schema1, allocator); + BigIntVector vector1 = (BigIntVector) root1.getVector("field1"); + vector1.allocateNew(10); + vector1.setSafe(0, 100L); + vector1.setValueCount(1); + root1.setRowCount(1); + roots.add(root1); + + // Create second root + Field field2 = new Field("field2", FieldType.nullable(new ArrowType.Int(64, true)), null); + Schema schema2 = new Schema(Collections.singletonList(field2)); + VectorSchemaRoot root2 = VectorSchemaRoot.create(schema2, allocator); + BigIntVector vector2 = (BigIntVector) root2.getVector("field2"); + vector2.allocateNew(10); + vector2.setSafe(0, 200L); + vector2.setValueCount(1); + root2.setRowCount(1); + roots.add(root2); + + // Serialize + byte[] data = ArrowUtil.serializeMultipleRoots(roots); + // Deserialize + List<VectorSchemaRoot> deserializedRoots = ArrowUtil.deserializeMultipleRoots(data); + + Assertions.assertEquals(2, deserializedRoots.size()); + VectorSchemaRoot resultRoot1 = deserializedRoots.get(0); + Assertions.assertEquals(1, resultRoot1.getRowCount()); + Assertions.assertEquals(100L, ((BigIntVector) resultRoot1.getVector("field1")).get(0)); + VectorSchemaRoot resultRoot2 = deserializedRoots.get(1); + Assertions.assertEquals(1, resultRoot2.getRowCount()); + Assertions.assertEquals(200L, ((BigIntVector) resultRoot2.getVector("field2")).get(0)); + + // Cleanup + roots.forEach(VectorSchemaRoot::close); + deserializedRoots.forEach(VectorSchemaRoot::close); + allocator.close(); + } + + @Test + void testSerializeAndDeserializeMetricsData() { + CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() + .setId(1L) + .setApp("linux") + .setMetrics("cpu") + .setTime(System.currentTimeMillis()) + .addField(CollectRep.Field.newBuilder().setName("usage").setType(1).build()) + .addValueRow(CollectRep.ValueRow.newBuilder().addColumn("50.5").build()) + .build(); + CollectRep.MetricsData metricsData1 = CollectRep.MetricsData.newBuilder() + .setId(1L) + .setApp("linux_1") + .setMetrics("cpu") + .setTime(System.currentTimeMillis()) + .addField(CollectRep.Field.newBuilder().setName("usage").setType(1).build()) + .addValueRow(CollectRep.ValueRow.newBuilder().addColumn("60.5").build()) + .build(); + List<CollectRep.MetricsData> list = Lists.newArrayList(metricsData, metricsData1); + + // Serialize + byte[] data = ArrowUtil.serializeMetricsData(list); + // Deserialize + List<CollectRep.MetricsData> deserializedList = ArrowUtil.deserializeMetricsData(data); + + Assertions.assertEquals(2, deserializedList.size()); + CollectRep.MetricsData result = deserializedList.get(0); + Assertions.assertEquals("linux", result.getApp()); + Assertions.assertEquals(1, result.getValues().size()); + Assertions.assertEquals("50.5", result.getValues().get(0).getColumns(0)); + + result = deserializedList.get(1); + Assertions.assertEquals("linux_1", result.getApp()); + Assertions.assertEquals(1, result.getValues().size()); + Assertions.assertEquals("60.5", result.getValues().get(0).getColumns(0)); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/BackoffUtilsTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/BackoffUtilsTest.java new file mode 100644 index 0000000..f97bb8e --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/BackoffUtilsTest.java @@ -0,0 +1,68 @@ +/* + * 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.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test case for {@link BackoffUtils} + */ +class BackoffUtilsTest { + + @Test + void shouldContinueAfterBackoff() { + ExponentialBackoff backoff = new ExponentialBackoff(10L, 100L); + boolean shouldContinue = BackoffUtils.shouldContinueAfterBackoff(backoff); + assertTrue(shouldContinue); + } + + @Test + void shouldNotContinueWhenInterrupted() { + Thread.currentThread().interrupt(); + ExponentialBackoff backoff = new ExponentialBackoff(10L, 100L); + boolean shouldContinue = BackoffUtils.shouldContinueAfterBackoff(backoff); + assertFalse(shouldContinue); + } + + @Test + void shouldHandleInterruptedException() throws InterruptedException { + final Thread mainThread = Thread.currentThread(); + ExponentialBackoff backoff = new ExponentialBackoff(1000L, 2000L); + + Thread interruptingThread = new Thread(() -> { + try { + // Give the main thread some time to enter the sleep + Thread.sleep(200); + mainThread.interrupt(); + } catch (InterruptedException ignored) { + } + }); + + interruptingThread.start(); + + boolean shouldContinue = BackoffUtils.shouldContinueAfterBackoff(backoff); + + interruptingThread.join(); + + assertFalse(shouldContinue); + assertTrue(Thread.currentThread().isInterrupted()); + } +} \ No newline at end of file diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/Base64UtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/Base64UtilTest.java new file mode 100644 index 0000000..1d653a8 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/Base64UtilTest.java @@ -0,0 +1,50 @@ +/* + * 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.util; + +import static org.apache.hertzbeat.common.util.Base64Util.isBase64; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link Base64Util} + */ +class Base64UtilTest { + + @Test + void testIsBase64() { + + String validBase64String = "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n"; + assertTrue(isBase64(validBase64String)); + + String invalidBase64String = "This is not a valid Base64 string!"; + assertFalse(isBase64(invalidBase64String)); + + String emptyString = ""; + assertFalse(isBase64(emptyString)); + + assertFalse(isBase64(null)); + + String whitespaceString = " "; + assertFalse(isBase64(whitespaceString)); + + String paddedBase64String = "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n==="; + assertFalse(isBase64(paddedBase64String)); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/ChineseCharacterCheckTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/ChineseCharacterCheckTest.java new file mode 100644 index 0000000..e98b669 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/ChineseCharacterCheckTest.java @@ -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.common.util; + +import com.github.javaparser.JavaParser; +import com.github.javaparser.ParseResult; +import com.github.javaparser.ast.CompilationUnit; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * Test case for checking Chinese characters in Java files + */ +@Slf4j +public class ChineseCharacterCheckTest { + + private static final Pattern CHINESE_CHAR_PATTERN = Pattern.compile("[\u4e00-\u9fa5]"); + private static final Set<String> EXCLUDED_FILES = new HashSet<>(Collections.singletonList("Metrics")); + private static final String MAIN_SOURCE_DIR = "src/main/java"; + private static final String TEST_SOURCE_DIR = "src/test/java"; + + private final JavaParser javaParser = new JavaParser(); + private final String sourceDir; + private final String testDir; + + public ChineseCharacterCheckTest() { + boolean isWindowsOs = System.getProperty("os.name").toLowerCase().startsWith("win"); + String separator = isWindowsOs ? "\\" : "/"; + this.sourceDir = MAIN_SOURCE_DIR.replace("/", separator); + this.testDir = TEST_SOURCE_DIR.replace("/", separator); + } + + @Test + void shouldNotContainChineseInComments() { + List<String> violations = scanForChineseCharacters(ScanTarget.COMMENTS); + assertNoChineseCharacters(violations); + } + + private List<String> scanForChineseCharacters(ScanTarget target) { + List<String> violations = new ArrayList<>(); + try (Stream<Path> paths = Files.walk(Paths.get(".."), FileVisitOption.FOLLOW_LINKS)) { + paths.filter(this::isValidJavaFile) + .forEach(path -> processFile(path, target, violations)); + } catch (IOException e) { + throw new RuntimeException("Failed to scan Java files", e); + } + return violations; + } + + private boolean isValidJavaFile(Path path) { + String pathStr = path.toString(); + return pathStr.endsWith(".java") + && (pathStr.contains(sourceDir) || pathStr.contains(testDir)) + && EXCLUDED_FILES.stream().noneMatch(pathStr::contains); + } + + private void processFile(Path path, ScanTarget target, List<String> violations) { + try { + ParseResult<CompilationUnit> parseResult = javaParser.parse(Files.newInputStream(path)); + parseResult.getResult().ifPresent(cu -> { + if (target.includeComments()) { + checkComments(cu, path, violations); + } + if (target.includeCode()) { + checkCode(cu, path, violations); + } + }); + } catch (Exception e) { + log.error("Error processing file: {}", path, e); + } + } + + private void checkComments(CompilationUnit cu, Path path, List<String> violations) { + cu.getAllContainedComments().stream() + .filter(comment -> CHINESE_CHAR_PATTERN.matcher(comment.getContent()).find()) + .forEach(comment -> violations.add(formatViolation(path, "comment", comment.getContent().trim()))); + } + + private void checkCode(CompilationUnit cu, Path path, List<String> violations) { + cu.findAll(com.github.javaparser.ast.expr.StringLiteralExpr.class).stream() + .filter(str -> CHINESE_CHAR_PATTERN.matcher(str.getValue()).find()) + .forEach(str -> violations.add(formatViolation(path, "code", str.getValue()))); + } + + private String formatViolation(Path path, String location, String content) { + return String.format("Chinese characters found in %s at %s: %s", + location, path.toAbsolutePath(), content); + } + + private void assertNoChineseCharacters(List<String> violations) { + Assertions.assertEquals(0, violations.size(), + () -> String.format("Found Chinese characters in files:%n%s", + String.join(System.lineSeparator(), violations))); + } + + private enum ScanTarget { + COMMENTS(true, false), + CODE(false, true), + ALL(true, true); + + private final boolean checkComments; + private final boolean checkCode; + + ScanTarget(boolean checkComments, boolean checkCode) { + this.checkComments = checkComments; + this.checkCode = checkCode; + } + + public boolean includeComments() { + return checkComments; + } + + public boolean includeCode() { + return checkCode; + } + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/CommonUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/CommonUtilTest.java new file mode 100644 index 0000000..c99e77c --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/CommonUtilTest.java @@ -0,0 +1,152 @@ +/* + * 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.util; + +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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link CommonUtil} + */ +class CommonUtilTest { + + @Test + void testParseStrInteger() { + assertEquals(10, CommonUtil.parseStrInteger("10")); + assertEquals(-10, CommonUtil.parseStrInteger("-10")); + assertEquals(+10, CommonUtil.parseStrInteger("+10")); + assertNotEquals(10_000, CommonUtil.parseStrInteger("10_000")); + assertNotEquals(16, CommonUtil.parseStrInteger("0x10")); + } + + @Test + void testParseStrDouble() { + assertEquals(10.125, CommonUtil.parseStrDouble("10.125")); + assertEquals(-10.125, CommonUtil.parseStrDouble("-10.125")); + assertEquals(100, CommonUtil.parseStrDouble("100d")); + assertEquals(100000, CommonUtil.parseStrDouble("100E3")); + assertEquals(0.1, CommonUtil.parseStrDouble("100E-3")); + assertNotEquals(10_000.125, CommonUtil.parseStrDouble("10_000.125")); + } + + @Test + void testParseIsNumeric() { + assertTrue(CommonUtil.isNumeric("1234")); + assertTrue(CommonUtil.isNumeric("6.954")); + assertFalse(CommonUtil.isNumeric("296.347%")); + assertFalse(CommonUtil.isNumeric("445_126")); + } + + @Test + void testParseTimeStrToSecond() { + assertEquals(36000, CommonUtil.parseTimeStrToSecond("10:00")); + assertEquals(43800, CommonUtil.parseTimeStrToSecond("12:10:00")); + assertNotEquals(43800, CommonUtil.parseTimeStrToSecond("2024-07-23 12:10:00")); + } + + @Test + void testParseDoubleStr() { + assertEquals("9.3454", CommonUtil.parseDoubleStr("9.345435345", null)); + assertEquals("9.3454", CommonUtil.parseDoubleStr("9.345435345%", "%")); + assertEquals("10", CommonUtil.parseDoubleStr("10%", "%")); + assertEquals("588", CommonUtil.parseDoubleStr("588Mb", "Mb")); + } + + @Test + void validateEmail() { + assertTrue(CommonUtil.validateEmail("tom@usthe.com")); + assertTrue(CommonUtil.validateEmail("demo@qq.com")); + assertFalse(CommonUtil.validateEmail("tom.usthe.com")); + } + + @Test + void validatePhoneNum() { + assertTrue(CommonUtil.validatePhoneNum("19234554432")); + assertTrue(CommonUtil.validatePhoneNum("13234554432")); + assertTrue(CommonUtil.validatePhoneNum("14234554432")); + assertTrue(CommonUtil.validatePhoneNum("16234554432")); + assertFalse(CommonUtil.validatePhoneNum("12234554432")); + assertFalse(CommonUtil.validatePhoneNum("11234554432")); + assertFalse(CommonUtil.validatePhoneNum("35234554432")); + assertFalse(CommonUtil.validatePhoneNum("46234554432")); + } + + @Test + void testGetMessageFromThrowable() { + assertEquals("throwable is null, unknown error.", CommonUtil.getMessageFromThrowable(null)); + assertEquals("throwable cause message", CommonUtil.getMessageFromThrowable(new Throwable(new Throwable("throwable cause message")))); + assertEquals("throwable message", CommonUtil.getMessageFromThrowable(new Throwable("throwable message"))); + assertEquals("throwable localizedMessage", CommonUtil.getMessageFromThrowable(new Throwable() { + @Override + public String getLocalizedMessage() { + return "throwable localizedMessage"; + } + })); + assertEquals("throwable toString", CommonUtil.getMessageFromThrowable(new Throwable() { + @Override + public String getMessage() { + return null; + } + + @Override + public String toString() { + return "throwable toString"; + } + })); + assertEquals("unknown error.", CommonUtil.getMessageFromThrowable(new Throwable() { + @Override + public String getMessage() { + return null; + } + + @Override + public String toString() { + return null; + } + })); + } + + @Test + void testRemoveBlankLine() { + assertEquals("line1\nline2\nline3", CommonUtil.removeBlankLine("line1\nline2\nline3")); + assertEquals("line1\nline3\nline4\nline6", CommonUtil.removeBlankLine("line1\n\nline3\nline4\n\n\nline6")); + assertEquals("", CommonUtil.removeBlankLine("")); + assertEquals("", CommonUtil.removeBlankLine("\n\n\n\n")); + } + + @Test + void testGetLangMappingValueFromI18nMap() { + Map<String, String> i18nMap = new HashMap<>(); + i18nMap.put("zh-CN", "中文"); + i18nMap.put("ja", null); + i18nMap.put("en-US", "English"); + assertEquals("中文", CommonUtil.getLangMappingValueFromI18nMap("zh-CN", i18nMap)); + assertEquals("English", CommonUtil.getLangMappingValueFromI18nMap("en-US", i18nMap)); + assertNull(CommonUtil.getLangMappingValueFromI18nMap("zh", new HashMap<>())); + assertNotNull(CommonUtil.getLangMappingValueFromI18nMap("ja", i18nMap)); + assertNotNull(CommonUtil.getLangMappingValueFromI18nMap("zh-TW", i18nMap)); + } + +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/ExponentialBackoffTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/ExponentialBackoffTest.java new file mode 100644 index 0000000..d82e8fb --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/ExponentialBackoffTest.java @@ -0,0 +1,54 @@ +/* + * 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.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link ExponentialBackoff} + */ +class ExponentialBackoffTest { + + @Test + void testProgressionAndCap() { + ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L); + Assertions.assertEquals(50L, backoff.nextDelay()); + Assertions.assertEquals(100L, backoff.nextDelay()); + Assertions.assertEquals(200L, backoff.nextDelay()); + Assertions.assertEquals(400L, backoff.nextDelay()); + Assertions.assertEquals(800L, backoff.nextDelay()); + Assertions.assertEquals(1000L, backoff.nextDelay()); + Assertions.assertEquals(1000L, backoff.nextDelay()); + } + + @Test + void testReset() { + ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L); + Assertions.assertEquals(50L, backoff.nextDelay()); + Assertions.assertEquals(100L, backoff.nextDelay()); + backoff.reset(); + Assertions.assertEquals(50L, backoff.nextDelay()); + } + + @Test + void testInvalidParams() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new ExponentialBackoff(0L, 1000L)); + Assertions.assertThrows(IllegalArgumentException.class, () -> new ExponentialBackoff(50L, 10L)); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/HertzBeatKeywordsUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/HertzBeatKeywordsUtilTest.java new file mode 100644 index 0000000..7f23e23 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/HertzBeatKeywordsUtilTest.java @@ -0,0 +1,54 @@ +/* + * 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.util; + +import org.apache.hertzbeat.common.constants.HertzBeatKeywordsEnum; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test case for {@link HertzBeatKeywordsUtil} + */ +class HertzBeatKeywordsUtilTest { + + @Test + void testVerifyKeywords_WithReservedKeyword_Instance() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> HertzBeatKeywordsUtil.verifyKeywords("instance") + ); + String expectedMessage = String.format( + "Field matches keyword `%s`, please set alias `%s`.", + HertzBeatKeywordsEnum.INSTANCE.getKeyword(), + HertzBeatKeywordsEnum.INSTANCE.getAlias() + ); + assertEquals(expectedMessage, exception.getMessage()); + } + + @Test + void testVerifyKeywords_WithValidField() { + assertDoesNotThrow(() -> HertzBeatKeywordsUtil.verifyKeywords("cpu_usage")); + assertDoesNotThrow(() -> HertzBeatKeywordsUtil.verifyKeywords("memory_used")); + assertDoesNotThrow(() -> HertzBeatKeywordsUtil.verifyKeywords("response_time")); + assertDoesNotThrow(() -> HertzBeatKeywordsUtil.verifyKeywords("metric_instance")); + assertDoesNotThrow(() -> HertzBeatKeywordsUtil.verifyKeywords(HertzBeatKeywordsEnum.INSTANCE.getAlias())); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/IntervalExpressionUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/IntervalExpressionUtilTest.java new file mode 100644 index 0000000..b23d101 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/IntervalExpressionUtilTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.util; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link IntervalExpressionUtil} + */ +class IntervalExpressionUtilTest { + + @Test + public void testValidNumberIntervalExpress() { + + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(5.0, null)); + + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(5.0, "")); + + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(null, "(3,7)")); + + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(5.0, "(3,7)")); + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(3.0, "(3,7)")); + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(7.0, "(3,7)")); + + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(3.0, "[3,7]")); + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(7.0, "[3,7]")); + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(2.0, "[3,7]")); + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(8.0, "[3,7]")); + + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(3.0, "[3,7)")); + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(6.9999, "[3,7)")); + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(7.0, "[3,7)")); + + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(4.0, "(3,7]")); + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(7.0, "(3,7]")); + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(3.0, "(3,7]")); + + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(-1000.0, "(-∞,5)")); + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(-1.0, "(-∞,5)")); + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(5.0, "(-∞,5)")); + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(10.0, "(-∞,5)")); + + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(1000.0, "(5,+∞)")); + assertTrue(IntervalExpressionUtil.validNumberIntervalExpress(10.0, "(5,+∞)")); + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(5.0, "(5,+∞)")); + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(0.0, "(5,+∞)")); + + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(5.0, "(3,7")); + assertFalse(IntervalExpressionUtil.validNumberIntervalExpress(5.0, "[3,7)3,7]")); + } + +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/IpDomainUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/IpDomainUtilTest.java new file mode 100644 index 0000000..3545010 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/IpDomainUtilTest.java @@ -0,0 +1,131 @@ +/* + * 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.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Collections; +import java.util.Enumeration; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +/** + * Test case for {@link IpDomainUtil} + */ +class IpDomainUtilTest { + + @Test + void validateIpDomain() { + assertTrue(IpDomainUtil.validateIpDomain("127.7.5.3")); + assertTrue(IpDomainUtil.validateIpDomain("255.255.4.3")); + assertTrue(IpDomainUtil.validateIpDomain("255.255.255.255")); + assertTrue(IpDomainUtil.validateIpDomain("tancloud.cn")); + assertTrue(IpDomainUtil.validateIpDomain("tancloud.com.cn")); + assertTrue(IpDomainUtil.validateIpDomain("student.dev.com.cn")); + assertTrue(IpDomainUtil.validateIpDomain("www.student.dev.com.cn")); + assertTrue(IpDomainUtil.validateIpDomain("www.baidu.com")); + assertTrue(IpDomainUtil.validateIpDomain("good.didi")); + assertTrue(IpDomainUtil.validateIpDomain("spider_rds.sogou")); + assertTrue(IpDomainUtil.validateIpDomain("_redis.hn.sogou")); + assertTrue(IpDomainUtil.validateIpDomain("host")); + assertTrue(IpDomainUtil.validateIpDomain("host-1")); + assertFalse(IpDomainUtil.validateIpDomain("www.baidu.com.")); + assertFalse(IpDomainUtil.validateIpDomain("good.")); + assertFalse(IpDomainUtil.validateIpDomain(".good.")); + assertFalse(IpDomainUtil.validateIpDomain("www.baidu..com")); + assertFalse(IpDomainUtil.validateIpDomain("www.baidu*com")); + assertFalse(IpDomainUtil.validateIpDomain("host$-1")); + } + + @Test + void isHasSchema() { + assertTrue(IpDomainUtil.isHasSchema("http://www.baidu.com")); + assertTrue(IpDomainUtil.isHasSchema("https://www.baidu.com")); + assertFalse(IpDomainUtil.isHasSchema("www.baidu.com")); + assertFalse(IpDomainUtil.isHasSchema("https_www.baidu.com")); + } + + @Test + void testGetLocalhostIp() throws SocketException { + + // Success + InetAddress mockedInetAddress = mock(Inet4Address.class); + when(mockedInetAddress.getHostAddress()).thenReturn("192.168.1.100"); + + NetworkInterface mockedNetworkInterface = mock(NetworkInterface.class); + when(mockedNetworkInterface.isLoopback()).thenReturn(false); + when(mockedNetworkInterface.isVirtual()).thenReturn(false); + when(mockedNetworkInterface.isUp()).thenReturn(true); + + Enumeration<InetAddress> inetAddresses = Collections.enumeration(Collections.singletonList(mockedInetAddress)); + when(mockedNetworkInterface.getInetAddresses()).thenReturn(inetAddresses); + + Enumeration<NetworkInterface> successNetworkInterfaces = Collections.enumeration(Collections.singletonList(mockedNetworkInterface)); + + try (MockedStatic<NetworkInterface> mockedStaticNetworkInterface = Mockito.mockStatic(NetworkInterface.class)) { + mockedStaticNetworkInterface.when(NetworkInterface::getNetworkInterfaces).thenReturn(successNetworkInterfaces); + String localhostIp = IpDomainUtil.getLocalhostIp(); + + assertEquals("192.168.1.100", localhostIp); + } + + // no network interface + Enumeration<NetworkInterface> noNetworkNetworkInterfaces = Collections.enumeration(Collections.emptyList()); + + try (MockedStatic<NetworkInterface> mockedStaticNetworkInterface = Mockito.mockStatic(NetworkInterface.class)) { + mockedStaticNetworkInterface.when(NetworkInterface::getNetworkInterfaces).thenReturn(noNetworkNetworkInterfaces); + String localhostIp = IpDomainUtil.getLocalhostIp(); + + assertNull(localhostIp); + } + + // throw exception + try (MockedStatic<NetworkInterface> mockedStaticNetworkInterface = Mockito.mockStatic(NetworkInterface.class)) { + mockedStaticNetworkInterface.when(NetworkInterface::getNetworkInterfaces).thenThrow(new RuntimeException("Test exception")); + String localhostIp = IpDomainUtil.getLocalhostIp(); + + assertNull(localhostIp); + } + + } + + @Test + void testCheckIpAddressType() { + + assertEquals(NetworkConstants.IPV4, IpDomainUtil.checkIpAddressType("192.168.1.1")); + assertEquals(NetworkConstants.IPV4, IpDomainUtil.checkIpAddressType("127.0.0.1")); + + assertEquals(NetworkConstants.IPV6, IpDomainUtil.checkIpAddressType("2001:0db8:85a3:0000:0000:8a2e:0370:7334")); + assertEquals(NetworkConstants.IPV6, IpDomainUtil.checkIpAddressType("::1")); + + assertEquals(NetworkConstants.IPV4, IpDomainUtil.checkIpAddressType("")); + assertEquals(NetworkConstants.IPV4, IpDomainUtil.checkIpAddressType(null)); + assertEquals(NetworkConstants.IPV4, IpDomainUtil.checkIpAddressType("invalid-ip")); + + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/JexlExpressionRunnerTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/JexlExpressionRunnerTest.java new file mode 100644 index 0000000..b9b7fd9 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/JexlExpressionRunnerTest.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.common.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.jexl3.JexlExpression; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link JexlExpressionRunner} + */ +public class JexlExpressionRunnerTest { + @Test + void evaluate() { + Map<String, Object> context = new HashMap<String, Object>(); + context.put("age", 2); + context.put("__age__", 2); + + String expression = "1 + 2"; + String expressionWithParam = "1 + age"; + String expressionWithName = "1 + __age__"; + + assertEquals(3, JexlExpressionRunner.evaluate(expression)); + assertEquals(3, JexlExpressionRunner.evaluate(expression, context)); + assertEquals(3, JexlExpressionRunner.evaluate(expressionWithParam, context)); + assertEquals(3, JexlExpressionRunner.evaluate(expressionWithName, context)); + + assertThrows(NullPointerException.class, () -> { + JexlExpressionRunner.evaluate(expression, null); + }); + + JexlExpression expObj = JexlExpressionRunner.compile(expression); + assertNotNull(expObj); + assertEquals(3, JexlExpressionRunner.evaluate(expObj, context)); + } + + @Test + void commonFuncs() { + assertEquals(true, JexlExpressionRunner.evaluate("equals(1, 1)")); + assertEquals(true, JexlExpressionRunner.evaluate("equals(null, null)")); + assertEquals(false, JexlExpressionRunner.evaluate("equals(null, 1)")); + assertEquals(true, JexlExpressionRunner.evaluate(""" + contains("abc", "a") + """)); + assertEquals(true, JexlExpressionRunner.evaluate(""" + contains("Abc", "a") + """)); + } + + @Test + void testJsonFunction() { + Map<String, Object> context = new HashMap<String, Object>(); + context.put("$.info", "{\"current\":537132,\"grantTotal\":63129,\"instanceCode\":\"CSDZMC\",\"timeMillis\":1754579874357}"); + + String expression = "json:apply($.info).current"; + + JexlExpression expObj = JexlExpressionRunner.compile(expression); + assertEquals(537132, JexlExpressionRunner.evaluate(expObj, context)); + + context.put("$.info", "{\"grantTotal\":63129,\"instanceCode\":\"CSDZMC\",\"timeMillis\":1754579874357}"); + assertNull(JexlExpressionRunner.evaluate(expObj, context)); + + context.clear(); + assertNull(JexlExpressionRunner.evaluate(expObj, context)); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/JexlTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/JexlTest.java new file mode 100644 index 0000000..fa502f8 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/JexlTest.java @@ -0,0 +1,603 @@ +/* + * 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.util; + +import com.google.common.collect.Maps; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.apache.commons.jexl3.JexlBuilder; +import org.apache.commons.jexl3.JexlContext; +import org.apache.commons.jexl3.JexlEngine; +import org.apache.commons.jexl3.JexlException; +import org.apache.commons.jexl3.JexlExpression; +import org.apache.commons.jexl3.JexlFeatures; +import org.apache.commons.jexl3.MapContext; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * test case for java expression language + */ +public class JexlTest { + + private JexlBuilder jexlBuilder; + + @BeforeEach + void setUp() { + ClassLoader classLoader = new ClassLoader() { + @Override + public String getName() { + return "jexl-class-loader"; + } + }; + JexlFeatures features = new JexlFeatures(); + features.annotation(false).loops(false).pragma(false) + .methodCall(false).lambda(false).newInstance(false).register(false); + jexlBuilder = new JexlBuilder().charset(StandardCharsets.UTF_8).cache(256).loader(classLoader) + .features(features).strict(true).silent(false).stackOverflow(40); + + } + + @Test + void testMultiExpression() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + JexlExpression e = jexl.createExpression("x * y"); + Object o = e.evaluate(context); + Assertions.assertEquals(8, o); + } + + @Test + void testDivisionExpression() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + JexlExpression e = jexl.createExpression("x / y"); + Object o = e.evaluate(context); + Assertions.assertEquals(8, o); + } + + @Test + void testAdditionExpression() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + JexlExpression e = jexl.createExpression("x + y"); + Object o = e.evaluate(context); + Assertions.assertEquals(9, o); + context.set("x", "hello"); + context.set("y", 3.0); + e = jexl.createExpression("x + y"); + o = e.evaluate(context); + Assertions.assertEquals("hello3.0", o); + } + + @Test + void testSubtractionExpression() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + JexlExpression e = jexl.createExpression("x - y"); + Object o = e.evaluate(context); + Assertions.assertEquals(7, o); + } + + @Test + void testModulusExpression() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 3); + JexlExpression e = jexl.createExpression("x % y"); + Object o = e.evaluate(context); + Assertions.assertEquals(2, o); + } + + @Test + void testComplicatedExpression() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + JexlExpression e = jexl.createExpression("x * y + 2 * x - y"); + Object o = e.evaluate(context); + Assertions.assertEquals(23, o); + } + + @Test + void testComplicatedExpressionWithParentheses() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + JexlExpression e = jexl.createExpression("(x * y) + (2 * x) - y"); + Object o = e.evaluate(context); + Assertions.assertEquals(23, o); + } + + @Test + void testComplicatedExpressionWithParenthesesAndSpaces() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + JexlExpression e = jexl.createExpression(" ( x * y ) + ( 2 * x ) - y "); + Object o = e.evaluate(context); + Assertions.assertEquals(23, o); + } + + @Test + void testComplicatedSpecialVariableNameExpression() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x.y", 8); + context.set("y", 1); + context.set("$.os.cpu.load_average.1m", 23); + context.set("$.os.load_average", 55); + context.set("$.fs.total.total_in_bytes", 20.0); + JexlExpression e = jexl.createExpression("x.y * y + 2 * x.y - y + $.os.cpu.load_average.1m + $.os.load_average + $.fs.total.total_in_bytes"); + Object o = e.evaluate(context); + Assertions.assertEquals(121.0, o); + } + + @Test + void testComplicatedSpecialVariableNameExpressionWithParenthesesAndSpaces() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x.y", 8); + context.set("y", 1); + context.set("$.os.cpu.load_average.1m", 23); + context.set("$.os.load_average", 55); + context.set("$.fs.total.total_in_bytes", 20.0); + JexlExpression e = jexl.createExpression(" ( x.y * y ) + ( 2 * x.y ) - y + $.os.cpu.load_average.1m + $.os.load_average + $.fs.total.total_in_bytes "); + Object o = e.evaluate(context); + Assertions.assertEquals(121.0, o); + } + + @Test + void testVariableAssignment() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("$.fs.total.total_in_bytes", 20.0); + JexlExpression e = jexl.createExpression(" $.fs.total.total_in_bytes "); + Object o = e.evaluate(context); + Assertions.assertEquals(20.0, o); + } + + @Test + void testSpecialVariableNameWithoutSpacesExpression() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("$.fs.total.free_in_bytes", 12.0); + context.set("$.fs.total.total_in_bytes", 23.0); + JexlExpression e = jexl.createExpression("(1-$.fs.total.free_in_bytes/$.fs.total.total_in_bytes)*100"); + Object o = e.evaluate(context); + Assertions.assertEquals(47.82608695652174, o); + } + + @Test + void testSpecialVariableNameWithSpacesExpression() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("$.fs.total.free_in_bytes", 12.0); + context.set("$.fs.total.total_in_bytes", 23.0); + JexlExpression e = jexl.createExpression("( 1 - $.fs.total.free_in_bytes / $.fs.total.total_in_bytes ) * 100"); + Object o = e.evaluate(context); + Assertions.assertEquals(47.82608695652174, o); + } + + @Test + void testJudgmentExpression() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + JexlExpression e = jexl.createExpression("x > y"); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + } + + @Test + void testJudgmentExpressionWithParentheses() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + JexlExpression e = jexl.createExpression("(x > y)"); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + } + + @Test + void testJudgmentExpressionWithParenthesesAndSpaces() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + JexlExpression e = jexl.createExpression(" ( x > y ) "); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + } + + @Test + void testJudgmentExpressionWithAndOperator() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + context.set("z", 2); + JexlExpression e = jexl.createExpression("x > y && x < z"); + Object o = e.evaluate(context); + Assertions.assertFalse((Boolean) o); + } + + @Test + void testJudgmentExpressionWithOrOperator() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + context.set("z", 2); + JexlExpression e = jexl.createExpression("x > y || x < z"); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + } + + @Test + void testJudgmentExpressionWithNotOperator() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + JexlExpression e = jexl.createExpression("!(x == 8)"); + Object o = e.evaluate(context); + Assertions.assertFalse((Boolean) o); + } + + @Test + void testJudgmentExpressionWithNotOperatorAndParentheses() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("$.fs.total.free_in_bytes", 8); + JexlExpression e = jexl.createExpression("!($.fs.total.free_in_bytes == 8)"); + Object o = e.evaluate(context); + Assertions.assertFalse((Boolean) o); + } + + @Test + void testCustomFunction() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("date", new DateFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 1); + JexlExpression e = jexl.createExpression("date:now(x,y)"); + Object o = e.evaluate(context); + String result = (String) o; + Assertions.assertTrue(result.endsWith("2")); + } + + @Test + void testZeroThrowException() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", 0); + JexlExpression e = jexl.createExpression("x / y"); + Assertions.assertThrows(JexlException.class, () -> e.evaluate(context)); + } + + @Test + void testNullThrowException() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", null); + JexlExpression e = jexl.createExpression("x / y"); + Assertions.assertThrows(JexlException.class, () -> e.evaluate(context)); + } + + @Test + void testEmptyStringThrowException() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", 8); + context.set("y", ""); + JexlExpression e = jexl.createExpression("x / y"); + Assertions.assertThrows(JexlException.class, () -> e.evaluate(context)); + } + + @Test + void testEqualsFunction() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", "hello"); + context.set("y", "hello"); + JexlExpression e = jexl.createExpression("sys:equals(x, y)"); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + } + + @Test + void testNotEqualsFunction() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", "hellos"); + context.set("y", "hello"); + JexlExpression e = jexl.createExpression("sys:equals(x, y)"); + Object o = e.evaluate(context); + Assertions.assertFalse((Boolean) o); + } + + @Test + void testContainsFunction() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put(null, new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", "hello"); + context.set("y", "e"); + JexlExpression e = jexl.createExpression("contains(x, y)"); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + } + + @Test + void testExistsFunction() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", "hello"); + JexlExpression e = jexl.createExpression("sys:exists(x)"); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + } + + @Test + void testExistsFunctionWithNull() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + JexlExpression e = jexl.createExpression("sys:exists(x)"); + Assertions.assertThrows(JexlException.class, () -> e.evaluate(context)); + } + + @Test + void testExistsFunctionWithEmptyString() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", ""); + JexlExpression e = jexl.createExpression("sys:exists(x)"); + Object o = e.evaluate(context); + Assertions.assertFalse((Boolean) o); + } + + @Test + void testExistsFunctionWithEmptyStringAndSpace() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", " "); + JexlExpression e = jexl.createExpression("sys:exists(x)"); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + } + + @Test + void testMatchesFunction() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", "hello"); + JexlExpression e = jexl.createExpression("sys:matches(x, '.*')"); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + } + + @Test + void testMatchesFunctionWithNull() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", null); + JexlExpression e = jexl.createExpression("sys:matches(x, '.*')"); + Object o = e.evaluate(context); + Assertions.assertFalse((Boolean) o); + } + + @Test + void testMatchesFunctionWithEmptyString() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", ""); + JexlExpression e = jexl.createExpression("sys:matches(x, '.*')"); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + } + + @Test + void testMatchesFunctionWithEmptyStringAndSpace() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", " "); + JexlExpression e = jexl.createExpression("sys:matches(x, '.*')"); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + } + + @Test + void testMatchesFunctionWithRegex() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", "hello"); + JexlExpression e = jexl.createExpression("sys:matches(x, 'he.*')"); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + } + + @Test + void testMatchesFunctionWithRegexNotMatch() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", "hello"); + JexlExpression e = jexl.createExpression("sys:matches(x, 'he')"); + Object o = e.evaluate(context); + Assertions.assertFalse((Boolean) o); + } + + @Test + void testMatchesFunctionWithRegexAndSpace() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put("sys", new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", "hello"); + JexlExpression e = jexl.createExpression("sys:matches(x, 'he.* ')"); + Object o = e.evaluate(context); + Assertions.assertFalse((Boolean) o); + } + + @Test + void testMatchesFunctionWithRegexAndSpace2() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put(null, new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", "Ubuntu50681269"); + JexlExpression e = jexl.createExpression("matches(x, '^[a-zA-Z0-9]+$')"); + Object o = e.evaluate(context); + Assertions.assertTrue((Boolean) o); + context.set("x", "Ubuntu_u50681269"); + o = e.evaluate(context); + Assertions.assertFalse((Boolean) o); + } + + @Test + void testNowFunction() { + Map<String, Object> functions = Maps.newLinkedHashMap(); + functions.put(null, new JexlCommonFunction()); + jexlBuilder.namespaces(functions); + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + JexlExpression e = jexl.createExpression("now() + '-0'"); + Object o = e.evaluate(context); + String result = (String) o; + Assertions.assertTrue(result.endsWith("-0")); + } + + @Test + void testAddString() { + JexlEngine jexl = jexlBuilder.create(); + JexlContext context = new MapContext(); + context.set("x", "Ubuntu"); + JexlExpression e = jexl.createExpression("x + \"-00000\""); + Object o = e.evaluate(context); + String result = (String) o; + e = jexl.createExpression("x + '-00000'"); + Assertions.assertEquals("Ubuntu-00000", result); + o = e.evaluate(context); + result = (String) o; + Assertions.assertEquals("Ubuntu-00000", result); + } + + @Test + void testUnconventionalMapping() { + // database_pages=Database pages + // name=User Commits Per Sec + JexlEngine jexl = jexlBuilder.create(); + Assertions.assertThrows(JexlException.class, () -> jexl.createExpression("Database pages")); + Assertions.assertDoesNotThrow(() -> jexl.createExpression("Database_pages")); + Assertions.assertThrows(JexlException.class, () -> jexl.createExpression("User Commits Per Sec")); + Assertions.assertThrows(JexlException.class, () -> jexl.createExpression("System I/O")); + Assertions.assertThrows(JexlException.class, () -> jexl.createExpression("User I/O")); + Assertions.assertThrows(JexlException.class, () -> jexl.createExpression("Library Cache Hit Ratio")); + Assertions.assertThrows(JexlException.class, () -> jexl.createExpression("Buffer Cache Hit Ratio")); + Assertions.assertThrows(JexlException.class, () -> jexl.createExpression("Page reads/sec")); + } + + @Test + void testRecException() { + JexlEngine jexl = jexlBuilder.create(); + Assertions.assertThrows(JexlException.class, () -> jexl.createExpression("new java.util.Date()")); + } + + @Test + void testNewObjectException() { + JexlEngine jexl = jexlBuilder.create(); + Assertions.assertThrows(JexlException.class, () -> jexl.createExpression("new java.lang.StringBuilder()")); + } + + @Test + void testMethodCallException() { + JexlEngine jexl = jexlBuilder.create(); + Assertions.assertThrows(JexlException.class, () -> jexl.createExpression("'string'.length()")); + Assertions.assertThrows(JexlException.class, () -> jexl.createExpression("System.currentTimeMillis()")); + } + + /** + * custom function + */ + public static class DateFunction { + public String now(Object... args) { + return System.currentTimeMillis() + "-" + args.length; + } + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/JsonUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/JsonUtilTest.java new file mode 100644 index 0000000..bf85344 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/JsonUtilTest.java @@ -0,0 +1,94 @@ +/* + * 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.util; + +import static org.apache.hertzbeat.common.util.JsonUtil.isJsonStr; +import static org.apache.hertzbeat.common.util.JsonUtil.isArray; +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import tools.jackson.core.type.TypeReference; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link JsonUtil} + */ +class JsonUtilTest { + + @Test + void toJson() { + Map<String, String> map = Map.of("test", "pro"); + + assertEquals("{\"test\":\"pro\"}", + JsonUtil.toJson(map)); + + assertNull(JsonUtil.toJson(null)); + } + + @Test + void testFromJson() { + assertNull(JsonUtil.fromJson("", new TypeReference<>() { + })); + assertNull(JsonUtil.fromJson((String) null, new TypeReference<>() { + })); + assertNull(JsonUtil.fromJson(" ", new TypeReference<>() { + })); + assertNull(JsonUtil.fromJson(" ", String.class)); + assertNull(JsonUtil.fromJson(" ")); + assertNull(JsonUtil.fromJson(null)); + assertNull(JsonUtil.fromJson("invalid")); + } + + @Test + void testIsJsonStr() { + String jsonString = "{\"name\":\"John\", \"age\":30"; + assertFalse(isJsonStr(jsonString)); + + assertFalse(isJsonStr("")); + + assertFalse(isJsonStr(null)); + + String whitespaceString = " "; + assertFalse(isJsonStr(whitespaceString)); + + jsonString = "This is just a plain string."; + assertFalse(isJsonStr(jsonString)); + + String jsonStringArrays = "[{\"name\":\"John\"}, {\"name\":\"Doe\"}]"; + assertTrue(isJsonStr(jsonStringArrays)); + + assertFalse(isJsonStr("{invalid}")); + } + + @Test + void testIsArray() { + String jsonStringNull = "{\"name\":\"John\", \"age\":30"; + assertEquals(Boolean.FALSE, isArray(jsonStringNull)); + + String jsonString = "{\"name\":\"John\", \"age\":30}"; + assertNotNull(isArray(jsonString)); + assertEquals(Boolean.FALSE, isArray(jsonString)); + + String jsonStringArrays = "[{\"name\":\"John\"}, {\"name\":\"Doe\"}]"; + assertNotNull(isArray(jsonStringArrays)); + assertEquals(Boolean.TRUE, isArray(jsonStringArrays)); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/LogUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/LogUtilTest.java new file mode 100644 index 0000000..176dbd3 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/LogUtilTest.java @@ -0,0 +1,117 @@ +/* + * 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.util; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.slf4j.Logger; +import java.lang.reflect.Method; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.contains; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class LogUtilTest { + + @Mock + private Logger mockLogger; + + private AutoCloseable mocks; + + @BeforeEach + void setUp() { + mocks = MockitoAnnotations.openMocks(this); + } + + @AfterEach + void tearDown() throws Exception { + if (mocks != null) { + mocks.close(); + } + } + + @Test + void testFormat_noParams_returnsOriginalMessage() throws Exception { + String original = "hello world"; + Method formatMethod = LogUtil.class.getDeclaredMethod("format", String.class, Object[].class); + formatMethod.setAccessible(true); + String formatted = (String) formatMethod.invoke(null, original, new Object[0]); + assertEquals(original, formatted); + } + + @Test + void testFormat_withParams_replacesPlaceholders() throws Exception { + String template = "hello,{0}, world {1}!"; + Method formatMethod = LogUtil.class.getDeclaredMethod("format", String.class, Object[].class); + formatMethod.setAccessible(true); + Object[] params = {"Alice", 123}; + String result = (String) formatMethod.invoke(null, template, params); + assertTrue(result.contains("hello,Alice")); + assertTrue(result.contains("world 123!")); + } + + @Test + void testDebug_noParams_logsRawMessage() { + when(mockLogger.isDebugEnabled()).thenReturn(true); + String msg = "test-debug"; + LogUtil.debug(mockLogger, msg); + verify(mockLogger).debug(contains(msg)); + } + + @Test + void testDebug_withParams_logsFormattedMessage() { + when(mockLogger.isDebugEnabled()).thenReturn(true); + LogUtil.debug(mockLogger, "user={0}", "Bob"); + verify(mockLogger).debug(contains("user=Bob")); + } + + @Test + void testInfo_levelOff_doesNotLog() { + when(mockLogger.isInfoEnabled()).thenReturn(false); + LogUtil.info(mockLogger, "should-not-log"); + verify(mockLogger, never()).info(anyString()); + } + + @Test + void testWarn_withException_logsMessageAndException() { + when(mockLogger.isWarnEnabled()).thenReturn(true); + RuntimeException ex = new RuntimeException("warn-ex"); + LogUtil.warn(mockLogger, ex, "warning {0}", "occurred"); + ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); + verify(mockLogger).warn(captor.capture(), eq(ex)); + assertTrue(captor.getValue().contains("warning occurred")); + } + + @Test + void testError_withExceptionAndParams_logsError() { + when(mockLogger.isErrorEnabled()).thenReturn(true); + RuntimeException ex = new RuntimeException("err"); + LogUtil.error(mockLogger, ex, "fail code {0}", 500); + ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); + verify(mockLogger).error(captor.capture(), eq(ex)); + assertTrue(captor.getValue().contains("fail code 500")); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/LruHashMapTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/LruHashMapTest.java new file mode 100644 index 0000000..58126e6 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/LruHashMapTest.java @@ -0,0 +1,77 @@ +/* + * 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.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link LruHashMap} + */ +class LruHashMapTest { + + @Test + void testLruHashMap() { + + int initThreshold = 3; + LruHashMap<Integer, String> initLruMap = new LruHashMap<>(initThreshold); + assertNotNull(initLruMap); + assertEquals(0, initLruMap.size()); + + int putAndGetThreshold = 2; + LruHashMap<Integer, String> putAndGetLruMap = new LruHashMap<>(putAndGetThreshold); + + putAndGetLruMap.put(1, "one"); + putAndGetLruMap.put(2, "two"); + + // Both entries should be present + assertEquals("one", putAndGetLruMap.get(1)); + assertEquals("two", putAndGetLruMap.get(2)); + + int evictionThreshold = 2; + LruHashMap<Integer, String> evictionLruMap = new LruHashMap<>(evictionThreshold); + + evictionLruMap.put(1, "one"); + evictionLruMap.put(2, "two"); + evictionLruMap.put(3, "three"); + + // The least recently used entry (1, "one") should be evicted + assertNull(evictionLruMap.get(1)); + assertEquals("two", evictionLruMap.get(2)); + assertEquals("three", evictionLruMap.get(3)); + + int accessOrderThreshold = 2; + LruHashMap<Integer, String> accessOrderLruMap = new LruHashMap<>(accessOrderThreshold); + + accessOrderLruMap.put(1, "one"); + accessOrderLruMap.put(2, "two"); + + // Access the first entry to make it recently used + accessOrderLruMap.get(1); + + accessOrderLruMap.put(3, "three"); + + // The least recently used entry (2, "two") should be evicted + assertEquals("one", accessOrderLruMap.get(1)); + assertNull(accessOrderLruMap.get(2)); + assertEquals("three", accessOrderLruMap.get(3)); + } + +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/MapCapUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/MapCapUtilTest.java new file mode 100644 index 0000000..b9ab4e9 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/MapCapUtilTest.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + +/** + * Test for {@link MapCapUtil} + */ +class MapCapUtilTest { + + @Test + public void testCalInitMap() { + int size = 0; + int expectedCapacity = (int) Math.ceil(size / 0.75); + int actualCapacity = MapCapUtil.calInitMap(size); + + assertEquals(expectedCapacity, actualCapacity); + + size = 10; + expectedCapacity = (int) Math.ceil(size / 0.75); + actualCapacity = MapCapUtil.calInitMap(size); + + assertEquals(expectedCapacity, actualCapacity); + } + +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/NetworkUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/NetworkUtilTest.java new file mode 100644 index 0000000..e358aa6 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/NetworkUtilTest.java @@ -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. + */ + +package org.apache.hertzbeat.common.util; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; + +/** + * Test for {@link NetworkUtil} + */ +public class NetworkUtilTest { + @Test + void common() { + assertFalse(NetworkUtil.isLinuxPlatform() && NetworkUtil.isWindowsPlatform()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/PairTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/PairTest.java new file mode 100644 index 0000000..5117246 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/PairTest.java @@ -0,0 +1,37 @@ +/* + * 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.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * Test for {@link Pair} + */ +public class PairTest { + @Test + void common() { + Pair<String, String> pair = Pair.of("key", "value"); + assertEquals("key", pair.getLeft()); + assertEquals("value", pair.getRight()); + + pair.setLeft("left"); + assertEquals("left", pair.getLeft()); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/ProtoJsonUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/ProtoJsonUtilTest.java new file mode 100644 index 0000000..908a579 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/ProtoJsonUtilTest.java @@ -0,0 +1,73 @@ +/* + * 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.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import org.apache.hertzbeat.common.util.entity.PersonTest.Person; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link ProtoJsonUtil} + */ +class ProtoJsonUtilTest { + + private Person samplePerson; + private String sampleJson; + + @BeforeEach + void setUp() { + + samplePerson = Person.newBuilder() + .setName("John Doe") + .setId(123) + .setEmail("john.doe@example.com") + .build(); + + sampleJson = "{ \"name\": \"John Doe\", \"id\": 123, \"email\": \"john.doe@example.com\" }"; + } + + @Test + void toJsonStr() throws InvalidProtocolBufferException { + + String json = ProtoJsonUtil.toJsonStr(samplePerson); + String expectedJson = JsonFormat.printer().print(samplePerson); + assertEquals(expectedJson, json); + + json = ProtoJsonUtil.toJsonStr(null); + assertNull(json); + } + + @Test + void toProtobuf() { + + Person.Builder builder = Person.newBuilder(); + Person person = (Person) ProtoJsonUtil.toProtobuf(sampleJson, builder); + assertEquals(samplePerson, person); + + String invalidJson = "{ \"name\": \"John Doe\", \"id\": \"not-a-number\" }"; + builder = Person.newBuilder(); + person = (Person) ProtoJsonUtil.toProtobuf(invalidJson, builder); + + assertNull(person); + } + +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/SnowFlakeIdGeneratorTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/SnowFlakeIdGeneratorTest.java new file mode 100644 index 0000000..4fa2b67 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/SnowFlakeIdGeneratorTest.java @@ -0,0 +1,37 @@ +/* + * 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.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link SnowFlakeIdGenerator} + */ +class SnowFlakeIdGeneratorTest { + + @Test + void generateId() { + // Note that because the front-end JS TS parses large numbers in json, + // the precision will be lost. UUID cannot exceed hexadecimal 0x1FFFFFFFFFFFFFF (less than 53bit) + for (int i = 0; i < 10000; i++) { + long id = SnowFlakeIdGenerator.generateId(); + Assertions.assertTrue(id < 0x1FFFFFFFFFFFFFL); + } + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/StrBufferTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/StrBufferTest.java new file mode 100644 index 0000000..98a5a93 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/StrBufferTest.java @@ -0,0 +1,151 @@ +/* + * 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.util; + +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 org.junit.jupiter.api.Test; + +/** + * test case for {@link StrBuffer} + */ + +class StrBufferTest { + + private static final String POSITIVE_INF = "+inf"; + private static final String NEGATIVE_INF = "-inf"; + private static final long POSITIVE_INF_VALUE = 0x7FF0000000000000L; + private static final long NEGATIVE_INF_VALUE = 0xFFF0000000000000L; + + @Test + void testRead() { + + StrBuffer buffer = new StrBuffer("hello"); + + assertEquals('h', buffer.read()); + assertEquals('e', buffer.read()); + assertEquals('l', buffer.read()); + assertEquals('l', buffer.read()); + assertEquals('o', buffer.read()); + assertThrows(IndexOutOfBoundsException.class, buffer::read); + } + + @Test + void testRollback() { + + StrBuffer buffer = new StrBuffer("hello"); + + assertEquals('h', buffer.read()); + buffer.rollback(); + assertEquals('h', buffer.read()); + buffer.read(); + buffer.read(); + buffer.rollback(); + assertEquals('l', buffer.read()); + } + + @Test + void testCharAt() { + + StrBuffer buffer = new StrBuffer("hello"); + + assertEquals('h', buffer.charAt(0)); + assertEquals('e', buffer.charAt(1)); + assertEquals('l', buffer.charAt(2)); + assertEquals('l', buffer.charAt(3)); + assertEquals('o', buffer.charAt(4)); + assertThrows(IndexOutOfBoundsException.class, () -> buffer.charAt(5)); + } + + @Test + void testToStr() { + + StrBuffer buffer = new StrBuffer("hello"); + + assertEquals("hello", buffer.toStr()); + buffer.read(); + assertEquals("ello", buffer.toStr()); + } + + @Test + void testToDouble() { + + StrBuffer buffer = new StrBuffer("123.45"); + assertEquals(123.45, buffer.toDouble()); + + buffer = new StrBuffer("+inf"); + assertEquals(POSITIVE_INF_VALUE, buffer.toDouble()); + + buffer = new StrBuffer("-inf"); + assertEquals(NEGATIVE_INF_VALUE, buffer.toDouble()); + } + + @Test + void testToLong() { + + StrBuffer buffer = new StrBuffer("12345"); + assertEquals(12345L, buffer.toLong()); + + buffer = new StrBuffer("+inf"); + assertEquals(POSITIVE_INF_VALUE, buffer.toLong()); + + buffer = new StrBuffer("-inf"); + assertEquals(NEGATIVE_INF_VALUE, buffer.toLong()); + } + + @Test + void testSkipBlankTabs() { + + StrBuffer buffer = new StrBuffer(" \t hello \t "); + buffer.skipBlankTabs(); + assertEquals("hello", buffer.toStr()); + } + + @Test + void testIsEmpty() { + + StrBuffer buffer = new StrBuffer(""); + assertTrue(buffer.isEmpty()); + + buffer = new StrBuffer(" \t "); + buffer.skipBlankTabs(); + assertTrue(buffer.isEmpty()); + + buffer = new StrBuffer("hello"); + assertFalse(buffer.isEmpty()); + } + + @Test + void testParseLong() { + + assertEquals(12345L, StrBuffer.parseLong("12345")); + assertEquals(POSITIVE_INF_VALUE, StrBuffer.parseLong(POSITIVE_INF)); + assertEquals(NEGATIVE_INF_VALUE, StrBuffer.parseLong(NEGATIVE_INF)); + } + + @Test + void testParseDouble() { + + assertEquals(123.45, StrBuffer.parseDouble("123.45")); + assertEquals(POSITIVE_INF_VALUE, StrBuffer.parseDouble("+inf")); + assertEquals(NEGATIVE_INF_VALUE, StrBuffer.parseDouble("-inf")); + } + +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/StrUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/StrUtilTest.java new file mode 100644 index 0000000..ba1ae2c --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/StrUtilTest.java @@ -0,0 +1,64 @@ +/* + * 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.util; + +import static org.apache.hertzbeat.common.util.StrUtil.analysisArgToList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link StrUtil} + */ +class StrUtilTest { + + @Test + void testAnalysisArgToList() { + List<String> result = analysisArgToList(null); + assertTrue(result.isEmpty()); + + result = analysisArgToList(""); + assertEquals(1, result.size()); + assertEquals("", result.get(0)); + + result = analysisArgToList("element"); + assertEquals(1, result.size()); + assertEquals("element", result.get(0)); + + result = analysisArgToList("one,two,three"); + assertEquals(3, result.size()); + assertEquals("one", result.get(0)); + assertEquals("two", result.get(1)); + assertEquals("three", result.get(2)); + + result = analysisArgToList(" one , two , three "); + assertEquals(3, result.size()); + assertEquals("one", result.get(0).trim()); + assertEquals("two", result.get(1).trim()); + assertEquals("three", result.get(2).trim()); + + result = analysisArgToList(",one,two,three,"); + assertEquals(4, result.size()); + assertEquals("", result.get(0)); + assertEquals("one", result.get(1)); + assertEquals("two", result.get(2)); + assertEquals("three", result.get(3)); + } + +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/TimePeriodUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/TimePeriodUtilTest.java new file mode 100644 index 0000000..5a46732 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/TimePeriodUtilTest.java @@ -0,0 +1,103 @@ +/* + * 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.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.time.Duration; +import java.time.Period; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAmount; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link SnowFlakeIdGenerator} + */ + +class TimePeriodUtilTest { + + @Test + void testParseTokenTime() { + + // Years + TemporalAmount result = TimePeriodUtil.parseTokenTime("1Y"); + assertTrue(result instanceof Period); + assertEquals(Period.ofYears(1), result); + + // Month + result = TimePeriodUtil.parseTokenTime("5M"); + assertTrue(result instanceof Period); + assertEquals(Period.ofMonths(5), result); + + // Day + result = TimePeriodUtil.parseTokenTime("3D"); + assertTrue(result instanceof Period); + assertEquals(Period.ofDays(3), result); + + // Week + result = TimePeriodUtil.parseTokenTime("3W"); + assertTrue(result instanceof Period); + assertEquals(Period.ofWeeks(3), result); + } + + @Test + void testParseTokenTimeDuration() { + + // Minute + TemporalAmount result = TimePeriodUtil.parseTokenTime("30m"); + assertTrue(result instanceof Duration); + assertEquals(Duration.ofMinutes(30), result); + + // Hour + result = TimePeriodUtil.parseTokenTime("2h"); + assertTrue(result instanceof Duration); + assertEquals(Duration.ofHours(2), result); + } + + @Test + void testParseTokenTimeLowerCaseMinute() { + // Lowercase Minute + TemporalAmount result = TimePeriodUtil.parseTokenTime("1m"); + assertTrue(result instanceof Duration); + assertEquals(Duration.ofMinutes(1), result); + } + + @Test + void testParseTokenTimeInvalidInput() { + + // null input + TemporalAmount result = TimePeriodUtil.parseTokenTime(null); + assertNull(result); + + // empty string + result = TimePeriodUtil.parseTokenTime(""); + assertNull(result); + + // string with length < 2 + result = TimePeriodUtil.parseTokenTime("1"); + assertNull(result); + + // invalid format (non-numeric) + Exception exception = assertThrows(DateTimeParseException.class, () -> TimePeriodUtil.parseTokenTime("abc")); + assertNotNull(exception.getMessage()); + } + +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/TimeZoneUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/TimeZoneUtilTest.java new file mode 100644 index 0000000..217faa4 --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/TimeZoneUtilTest.java @@ -0,0 +1,86 @@ +/* + * 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.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.Locale; +import java.util.TimeZone; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * test case for {@link TimeZoneUtil} + */ + +class TimeZoneUtilTest { + + private TimeZone defaultTimeZone; + private Locale defaultLocale; + + @BeforeEach + void setUp() { + + defaultTimeZone = TimeZone.getDefault(); + defaultLocale = Locale.getDefault(); + } + + @AfterEach + void tearDown() { + + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + + @Test + void testSetTimeZoneAndLocale() { + + TimeZoneUtil.setTimeZoneAndLocale("America/New_York", "en_US"); + assertEquals("America/New_York", TimeZone.getDefault().getID()); + assertEquals(new Locale("en", "US"), Locale.getDefault()); + } + + @Test + void testSetTimeZone() { + + TimeZoneUtil.setTimeZone("Asia/Tokyo"); + assertEquals("Asia/Tokyo", TimeZone.getDefault().getID()); + + + TimeZoneUtil.setTimeZone(""); + assertEquals("Asia/Tokyo", TimeZone.getDefault().getID()); + + TimeZoneUtil.setTimeZone(null); + assertEquals("Asia/Tokyo", TimeZone.getDefault().getID()); + } + + @Test + void testSetLocale() { + + TimeZoneUtil.setLocale("fr_FR"); + assertEquals(new Locale("fr", "FR"), Locale.getDefault()); + + + TimeZoneUtil.setLocale(""); + assertEquals(new Locale("fr", "FR"), Locale.getDefault()); + + TimeZoneUtil.setLocale(null); + assertEquals(new Locale("fr", "FR"), Locale.getDefault()); + } + +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/entity/PersonTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/entity/PersonTest.java new file mode 100644 index 0000000..6135c5f --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/entity/PersonTest.java @@ -0,0 +1,917 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: person.proto + +package org.apache.hertzbeat.common.util.entity; + +@SuppressWarnings("all") +public final class PersonTest { + private PersonTest() { + } + + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistryLite registry) { + } + + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistry registry) { + registerAllExtensions( + (com.google.protobuf.ExtensionRegistryLite) registry); + } + + public interface PersonOrBuilder extends + // @@protoc_insertion_point(interface_extends:org.apache.hertzbeat.common.util.entity.Person) + com.google.protobuf.MessageOrBuilder { + + /** + * <code>string name = 1;</code> + * + * @return The name. + */ + java.lang.String getName(); + + /** + * <code>string name = 1;</code> + * + * @return The bytes for name. + */ + com.google.protobuf.ByteString + getNameBytes(); + + /** + * <code>int32 id = 2;</code> + * + * @return The id. + */ + int getId(); + + /** + * <code>string email = 3;</code> + * + * @return The email. + */ + java.lang.String getEmail(); + + /** + * <code>string email = 3;</code> + * + * @return The bytes for email. + */ + com.google.protobuf.ByteString + getEmailBytes(); + } + + /** + * Protobuf type {@code org.apache.hertzbeat.common.util.entity.Person} + */ + public static final class Person extends + com.google.protobuf.GeneratedMessageV3 implements + // @@protoc_insertion_point(message_implements:org.apache.hertzbeat.common.util.entity.Person) + PersonOrBuilder { + private static final long serialVersionUID = 0L; + + // Use Person.newBuilder() to construct. + private Person(com.google.protobuf.GeneratedMessageV3.Builder<?> builder) { + super(builder); + } + + private Person() { + name_ = ""; + email_ = ""; + } + + @java.lang.Override + @SuppressWarnings({"unused"}) + protected java.lang.Object newInstance( + UnusedPrivateParameter unused) { + return new Person(); + } + + @java.lang.Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.apache.hertzbeat.common.util.entity.PersonTest.internal_static_org_apache_hertzbeat_common_util_entity_Person_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.apache.hertzbeat.common.util.entity.PersonTest.internal_static_org_apache_hertzbeat_common_util_entity_Person_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.apache.hertzbeat.common.util.entity.PersonTest.Person.class, org.apache.hertzbeat.common.util.entity.PersonTest.Person.Builder.class); + } + + public static final int NAME_FIELD_NUMBER = 1; + private volatile java.lang.Object name_; + + /** + * <code>string name = 1;</code> + * + * @return The name. + */ + @java.lang.Override + public java.lang.String getName() { + java.lang.Object ref = name_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + name_ = s; + return s; + } + } + + /** + * <code>string name = 1;</code> + * + * @return The bytes for name. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getNameBytes() { + java.lang.Object ref = name_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + name_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int ID_FIELD_NUMBER = 2; + private int id_; + + /** + * <code>int32 id = 2;</code> + * + * @return The id. + */ + @java.lang.Override + public int getId() { + return id_; + } + + public static final int EMAIL_FIELD_NUMBER = 3; + private volatile java.lang.Object email_; + + /** + * <code>string email = 3;</code> + * + * @return The email. + */ + @java.lang.Override + public java.lang.String getEmail() { + java.lang.Object ref = email_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + email_ = s; + return s; + } + } + + /** + * <code>string email = 3;</code> + * + * @return The bytes for email. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getEmailBytes() { + java.lang.Object ref = email_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + email_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + private byte memoizedIsInitialized = -1; + + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(name_)) { + com.google.protobuf.GeneratedMessageV3.writeString(output, 1, name_); + } + if (id_ != 0) { + output.writeInt32(2, id_); + } + if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(email_)) { + com.google.protobuf.GeneratedMessageV3.writeString(output, 3, email_); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(name_)) { + size += com.google.protobuf.GeneratedMessageV3.computeStringSize(1, name_); + } + if (id_ != 0) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(2, id_); + } + if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(email_)) { + size += com.google.protobuf.GeneratedMessageV3.computeStringSize(3, email_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof org.apache.hertzbeat.common.util.entity.PersonTest.Person)) { + return super.equals(obj); + } + org.apache.hertzbeat.common.util.entity.PersonTest.Person other = (org.apache.hertzbeat.common.util.entity.PersonTest.Person) obj; + + if (!getName() + .equals(other.getName())) return false; + if (getId() + != other.getId()) return false; + if (!getEmail() + .equals(other.getEmail())) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + NAME_FIELD_NUMBER; + hash = (53 * hash) + getName().hashCode(); + hash = (37 * hash) + ID_FIELD_NUMBER; + hash = (53 * hash) + getId(); + hash = (37 * hash) + EMAIL_FIELD_NUMBER; + hash = (53 * hash) + getEmail().hashCode(); + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input); + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { + return newBuilder(); + } + + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + + public static Builder newBuilder(org.apache.hertzbeat.common.util.entity.PersonTest.Person prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + + /** + * Protobuf type {@code org.apache.hertzbeat.common.util.entity.Person} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageV3.Builder<Builder> implements + // @@protoc_insertion_point(builder_implements:org.apache.hertzbeat.common.util.entity.Person) + org.apache.hertzbeat.common.util.entity.PersonTest.PersonOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.apache.hertzbeat.common.util.entity.PersonTest.internal_static_org_apache_hertzbeat_common_util_entity_Person_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.apache.hertzbeat.common.util.entity.PersonTest.internal_static_org_apache_hertzbeat_common_util_entity_Person_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.apache.hertzbeat.common.util.entity.PersonTest.Person.class, org.apache.hertzbeat.common.util.entity.PersonTest.Person.Builder.class); + } + + // Construct using org.apache.hertzbeat.common.util.entity.PersonTest.Person.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + super(parent); + + } + + @java.lang.Override + public Builder clear() { + super.clear(); + name_ = ""; + + id_ = 0; + + email_ = ""; + + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.apache.hertzbeat.common.util.entity.PersonTest.internal_static_org_apache_hertzbeat_common_util_entity_Person_descriptor; + } + + @java.lang.Override + public org.apache.hertzbeat.common.util.entity.PersonTest.Person getDefaultInstanceForType() { + return org.apache.hertzbeat.common.util.entity.PersonTest.Person.getDefaultInstance(); + } + + @java.lang.Override + public org.apache.hertzbeat.common.util.entity.PersonTest.Person build() { + org.apache.hertzbeat.common.util.entity.PersonTest.Person result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public org.apache.hertzbeat.common.util.entity.PersonTest.Person buildPartial() { + org.apache.hertzbeat.common.util.entity.PersonTest.Person result = new org.apache.hertzbeat.common.util.entity.PersonTest.Person(this); + result.name_ = name_; + result.id_ = id_; + result.email_ = email_; + onBuilt(); + return result; + } + + @java.lang.Override + public Builder clone() { + return super.clone(); + } + + @java.lang.Override + public Builder setField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return super.setField(field, value); + } + + @java.lang.Override + public Builder clearField( + com.google.protobuf.Descriptors.FieldDescriptor field) { + return super.clearField(field); + } + + @java.lang.Override + public Builder clearOneof( + com.google.protobuf.Descriptors.OneofDescriptor oneof) { + return super.clearOneof(oneof); + } + + @java.lang.Override + public Builder setRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + int index, java.lang.Object value) { + return super.setRepeatedField(field, index, value); + } + + @java.lang.Override + public Builder addRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return super.addRepeatedField(field, value); + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.apache.hertzbeat.common.util.entity.PersonTest.Person) { + return mergeFrom((org.apache.hertzbeat.common.util.entity.PersonTest.Person) other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.apache.hertzbeat.common.util.entity.PersonTest.Person other) { + if (other == org.apache.hertzbeat.common.util.entity.PersonTest.Person.getDefaultInstance()) + return this; + if (!other.getName().isEmpty()) { + name_ = other.name_; + onChanged(); + } + if (other.getId() != 0) { + setId(other.getId()); + } + if (!other.getEmail().isEmpty()) { + email_ = other.email_; + onChanged(); + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: { + name_ = input.readStringRequireUtf8(); + + break; + } // case 10 + case 16: { + id_ = input.readInt32(); + + break; + } // case 16 + case 26: { + email_ = input.readStringRequireUtf8(); + + break; + } // case 26 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + + private java.lang.Object name_ = ""; + + /** + * <code>string name = 1;</code> + * + * @return The name. + */ + public java.lang.String getName() { + java.lang.Object ref = name_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + name_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + + /** + * <code>string name = 1;</code> + * + * @return The bytes for name. + */ + public com.google.protobuf.ByteString + getNameBytes() { + java.lang.Object ref = name_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + name_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + /** + * <code>string name = 1;</code> + * + * @param value The name to set. + * @return This builder for chaining. + */ + public Builder setName( + java.lang.String value) { + if (value == null) { + throw new NullPointerException(); + } + + name_ = value; + onChanged(); + return this; + } + + /** + * <code>string name = 1;</code> + * + * @return This builder for chaining. + */ + public Builder clearName() { + + name_ = getDefaultInstance().getName(); + onChanged(); + return this; + } + + /** + * <code>string name = 1;</code> + * + * @param value The bytes for name to set. + * @return This builder for chaining. + */ + public Builder setNameBytes( + com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + checkByteStringIsUtf8(value); + + name_ = value; + onChanged(); + return this; + } + + private int id_; + + /** + * <code>int32 id = 2;</code> + * + * @return The id. + */ + @java.lang.Override + public int getId() { + return id_; + } + + /** + * <code>int32 id = 2;</code> + * + * @param value The id to set. + * @return This builder for chaining. + */ + public Builder setId(int value) { + + id_ = value; + onChanged(); + return this; + } + + /** + * <code>int32 id = 2;</code> + * + * @return This builder for chaining. + */ + public Builder clearId() { + + id_ = 0; + onChanged(); + return this; + } + + private java.lang.Object email_ = ""; + + /** + * <code>string email = 3;</code> + * + * @return The email. + */ + public java.lang.String getEmail() { + java.lang.Object ref = email_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + email_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + + /** + * <code>string email = 3;</code> + * + * @return The bytes for email. + */ + public com.google.protobuf.ByteString + getEmailBytes() { + java.lang.Object ref = email_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + email_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + /** + * <code>string email = 3;</code> + * + * @param value The email to set. + * @return This builder for chaining. + */ + public Builder setEmail( + java.lang.String value) { + if (value == null) { + throw new NullPointerException(); + } + + email_ = value; + onChanged(); + return this; + } + + /** + * <code>string email = 3;</code> + * + * @return This builder for chaining. + */ + public Builder clearEmail() { + + email_ = getDefaultInstance().getEmail(); + onChanged(); + return this; + } + + /** + * <code>string email = 3;</code> + * + * @param value The bytes for email to set. + * @return This builder for chaining. + */ + public Builder setEmailBytes( + com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + checkByteStringIsUtf8(value); + + email_ = value; + onChanged(); + return this; + } + + @java.lang.Override + public final Builder setUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.setUnknownFields(unknownFields); + } + + @java.lang.Override + public final Builder mergeUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.mergeUnknownFields(unknownFields); + } + + + // @@protoc_insertion_point(builder_scope:org.apache.hertzbeat.common.util.entity.Person) + } + + // @@protoc_insertion_point(class_scope:org.apache.hertzbeat.common.util.entity.Person) + private static final org.apache.hertzbeat.common.util.entity.PersonTest.Person DEFAULT_INSTANCE; + + static { + DEFAULT_INSTANCE = new org.apache.hertzbeat.common.util.entity.PersonTest.Person(); + } + + public static org.apache.hertzbeat.common.util.entity.PersonTest.Person getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser<Person> + PARSER = new com.google.protobuf.AbstractParser<Person>() { + @java.lang.Override + public Person parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser<Person> parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser<Person> getParserForType() { + return PARSER; + } + + @java.lang.Override + public org.apache.hertzbeat.common.util.entity.PersonTest.Person getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_org_apache_hertzbeat_common_util_entity_Person_descriptor; + private static final + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internal_static_org_apache_hertzbeat_common_util_entity_Person_fieldAccessorTable; + + public static com.google.protobuf.Descriptors.FileDescriptor + getDescriptor() { + return descriptor; + } + + private static com.google.protobuf.Descriptors.FileDescriptor + descriptor; + + static { + java.lang.String[] descriptorData = { + "\n\014person.proto\022\'org.apache.hertzbeat.com" + + "mon.util.entity\"1\n\006Person\022\014\n\004name\030\001 \001(\t\022" + + "\n\n\002id\030\002 \001(\005\022\r\n\005email\030\003 \001(\tB\014B\nPersonTest" + + "b\006proto3" + }; + descriptor = com.google.protobuf.Descriptors.FileDescriptor + .internalBuildGeneratedFileFrom(descriptorData, + new com.google.protobuf.Descriptors.FileDescriptor[]{ + }); + internal_static_org_apache_hertzbeat_common_util_entity_Person_descriptor = + getDescriptor().getMessageTypes().get(0); + internal_static_org_apache_hertzbeat_common_util_entity_Person_fieldAccessorTable = new + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( + internal_static_org_apache_hertzbeat_common_util_entity_Person_descriptor, + new java.lang.String[]{"Name", "Id", "Email",}); + } + + // @@protoc_insertion_point(outer_class_scope) +} diff --git a/hertzbeat-common-core/src/test/proto/person.proto b/hertzbeat-common-core/src/test/proto/person.proto new file mode 100644 index 0000000..6335e76 --- /dev/null +++ b/hertzbeat-common-core/src/test/proto/person.proto @@ -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. + */ + +syntax = "proto3"; + +package org.apache.hertzbeat.common.util.entity; + +option java_outer_classname = "PersonTest"; + +message Person { + string name = 1; + int32 id = 2; + string email = 3; +} diff --git a/hertzbeat-common-spring/pom.xml b/hertzbeat-common-spring/pom.xml new file mode 100644 index 0000000..7aafb4d --- /dev/null +++ b/hertzbeat-common-spring/pom.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>hertzbeat</artifactId> + <groupId>org.apache.hertzbeat</groupId> + <version>2.0-SNAPSHOT</version> + </parent> + + <modelVersion>4.0.0</modelVersion> + + <artifactId>hertzbeat-common-spring</artifactId> + <name>${project.artifactId}</name> + <description>Spring Boot, configuration, validation, and JPA integration built on common-core.</description> + + <dependencies> + <!-- hertzbeat-common-core dependency --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-common-core</artifactId> + </dependency> + + <!-- spring --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-configuration-processor</artifactId> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-autoconfigure</artifactId> + </dependency> + <!-- jpa --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + </dependency> + <!-- validation --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + <!-- swagger --> + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <scope>provided</scope> + </dependency> + </dependencies> + +</project> diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/cache/CacheFactory.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/cache/CacheFactory.java new file mode 100644 index 0000000..a58321b --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/cache/CacheFactory.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.cache; + +import java.time.Duration; +import java.util.List; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.apache.hertzbeat.common.entity.alerter.NoticeRule; + +/** + * common cache factory + */ +public final class CacheFactory { + private CacheFactory() {} + + private static final CommonCacheService<String, Object> COMMON_CACHE = + new CaffeineCacheServiceImpl<>(1, 1000, Duration.ofDays(1), false); + + /** + * get notice cache + * @return caffeine cache + */ + @SuppressWarnings("unchecked") + public static List<NoticeRule> getNoticeCache() { + return (List<NoticeRule>) COMMON_CACHE.get(CommonConstants.CACHE_NOTICE_RULE); + } + + /** + * set notice cache + * @param noticeRules notice rules + */ + public static void setNoticeCache(List<NoticeRule> noticeRules) { + COMMON_CACHE.put(CommonConstants.CACHE_NOTICE_RULE, noticeRules); + } + + /** + * clear notice cache + */ + public static void clearNoticeCache() { + COMMON_CACHE.remove(CommonConstants.CACHE_NOTICE_RULE); + } + + /** + * get alert silence cache + * @return caffeine cache + */ + @SuppressWarnings("unchecked") + public static List<AlertSilence> getAlertSilenceCache() { + return (List<AlertSilence>) COMMON_CACHE.get(CommonConstants.CACHE_ALERT_SILENCE); + } + + /** + * set alert silence cache + * @param alertSilences alert silences + */ + public static void setAlertSilenceCache(List<AlertSilence> alertSilences) { + COMMON_CACHE.put(CommonConstants.CACHE_ALERT_SILENCE, alertSilences); + } + + /** + * clear alert silence cache + */ + public static void clearAlertSilenceCache() { + COMMON_CACHE.remove(CommonConstants.CACHE_ALERT_SILENCE); + } + + /** + * get metrics alert define cache + * @return caffeine cache + */ + @SuppressWarnings("unchecked") + public static List<AlertDefine> getMetricsAlertDefineCache() { + return (List<AlertDefine>) COMMON_CACHE.get(CommonConstants.METRIC_CACHE_ALERT_DEFINE); + } + + /** + * set metrics alert define cache + * @param alertDefines alert defines + */ + public static void setMetricsAlertDefineCache(List<AlertDefine> alertDefines) { + COMMON_CACHE.put(CommonConstants.METRIC_CACHE_ALERT_DEFINE, alertDefines); + } + + /** + * clear metrics alert define cache + */ + public static void clearMetricsAlertDefineCache() { + COMMON_CACHE.remove(CommonConstants.METRIC_CACHE_ALERT_DEFINE); + } + + /** + * get log alert define cache + * @return caffeine cache + */ + @SuppressWarnings("unchecked") + public static List<AlertDefine> getLogAlertDefineCache() { + return (List<AlertDefine>) COMMON_CACHE.get(CommonConstants.LOG_CACHE_ALERT_DEFINE); + } + + /** + * set log alert define cache + * @param alertDefines alert defines + */ + public static void setLogAlertDefineCache(List<AlertDefine> alertDefines) { + COMMON_CACHE.put(CommonConstants.LOG_CACHE_ALERT_DEFINE, alertDefines); + } + + /** + * clear log alert define cache + */ + public static void clearLogAlertDefineCache() { + COMMON_CACHE.remove(CommonConstants.LOG_CACHE_ALERT_DEFINE); + } + + /** + * clear alert define cache + */ + public static void clearAlertDefineCache() { + clearLogAlertDefineCache(); + clearMetricsAlertDefineCache(); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/cache/CaffeineCacheServiceImpl.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/cache/CaffeineCacheServiceImpl.java new file mode 100644 index 0000000..7163350 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/cache/CaffeineCacheServiceImpl.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.cache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.time.Duration; + +/** + * caffeine cache impl + */ +public class CaffeineCacheServiceImpl<K, V> implements CommonCacheService<K, V> { + private final Cache<K, V> cache; + + public CaffeineCacheServiceImpl(final int initialCapacity, final long maximumSize, final Duration expireAfterWrite, final boolean useWeakKey) { + if (useWeakKey) { + this.cache = Caffeine.newBuilder() + .weakKeys() + .initialCapacity(initialCapacity) + .maximumSize(maximumSize) + .expireAfterWrite(expireAfterWrite) + .build(); + } else { + this.cache = Caffeine.newBuilder() + .initialCapacity(initialCapacity) + .maximumSize(maximumSize) + .expireAfterWrite(expireAfterWrite) + .build(); + } + } + + @Override + public V get(K key) { + return cache.getIfPresent(key); + } + + @Override + public void put(K key, V value) { + cache.put(key, value); + } + + @Override + public V putAndGetOld(K key, V value) { + V oldValue = cache.getIfPresent(key); + cache.put(key, value); + return oldValue; + } + + @Override + public boolean containsKey(K key) { + return cache.asMap().containsKey(key); + } + + @Override + public V remove(K key) { + V value = cache.getIfPresent(key); + this.cache.invalidate(key); + this.cache.cleanUp(); + return value; + } + + @Override + public boolean clear() { + this.cache.invalidateAll(); + this.cache.cleanUp(); + return true; + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/CommonConfig.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/CommonConfig.java new file mode 100644 index 0000000..06a24c9 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/CommonConfig.java @@ -0,0 +1,42 @@ +/* + * 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.config; + +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; + +/** + * common module config + */ + +@AutoConfiguration +@ComponentScan(basePackages = ConfigConstants.PkgConstant.PKG + + SignConstants.DOT + + ConfigConstants.FunctionModuleConstants.COMMON) +@EnableConfigurationProperties({CommonProperties.class, VirtualThreadPropertiesBinding.class, SmsConfigBinding.class}) +public class CommonConfig { + + @Bean + public VirtualThreadProperties virtualThreadProperties(VirtualThreadPropertiesBinding binding) { + return binding.toRuntimeProperties(); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/CommonProperties.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/CommonProperties.java new file mode 100644 index 0000000..b88cc9c --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/CommonProperties.java @@ -0,0 +1,167 @@ +/* + * 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.config; + +import lombok.Getter; +import lombok.Setter; +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * common module properties + */ + +@Getter +@Setter +@ConfigurationProperties(prefix = + ConfigConstants.FunctionModuleConstants.COMMON) +public class CommonProperties { + + /** + * secret key for password aes entry, must 16 bits + */ + private String secret; + + /** + * data queue impl + */ + private DataQueueProperties queue; + + /** + * data queue properties + */ + @Getter + @Setter + public static class DataQueueProperties { + + private QueueType type = QueueType.Memory; + + private KafkaProperties kafka; + + private RedisProperties redis; + + } + + /** + * data queue type + */ + public enum QueueType { + /** in memory **/ + Memory, + /** kafka **/ + Kafka, + /** with netty connect **/ + Netty, + /** rabbit mq **/ + Rabbit_Mq, + /** redis **/ + Redis + } + + /** + * redis data queue properties + */ + @Getter + @Setter + public static class RedisProperties { + + /** + * redis server host. + */ + private String redisHost; + + /** + * redis server port. + */ + private int redisPort; + + /** + * Queue name for metrics data to alerter + */ + private String metricsDataQueueNameToAlerter; + + /** + * Queue name for metrics data to persistent storage + */ + private String metricsDataQueueNameToPersistentStorage; + + /** + * Queue name for metrics data to real-time storage + */ + private String metricsDataQueueNameToRealTimeStorage; + + /** + * Queue name for service discovery + */ + private String metricsDataQueueNameForServiceDiscovery; + + /** + * Queue name for alerts data + */ + private String alertsDataQueueName; + + /** + * Queue name for log entry data + */ + private String logEntryQueueName; + + /** + * Queue name for log entry data to storage + */ + private String logEntryToStorageQueueName; + + /** + * Timeout for blocking wait in seconds (defaults to 1 second if not configured) + */ + private Long waitTimeout; + + } + + /** + * kafka data queue properties + */ + @Getter + @Setter + public static class KafkaProperties extends BaseKafkaProperties { + + /** + * metrics data topic + */ + private String metricsDataTopic; + /** + * metrics data to storage topic + */ + private String metricsDataToStorageTopic; + /** + * service discovery data topic + */ + private String serviceDiscoveryDataTopic; + /** + * alerts data topic + */ + private String alertsDataTopic; + /** + * log entry data topic + */ + private String logEntryDataTopic; + /** + * log entry data to storage topic + */ + private String logEntryDataToStorageTopic; + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/SmsConfigBinding.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/SmsConfigBinding.java new file mode 100644 index 0000000..bc0faac --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/SmsConfigBinding.java @@ -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.config; + +import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Spring Boot binding adapter for {@link SmsConfig}. + */ +@ConfigurationProperties(prefix = "alerter.sms") +public class SmsConfigBinding extends SmsConfig { +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/VirtualThreadPropertiesBinding.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/VirtualThreadPropertiesBinding.java new file mode 100644 index 0000000..e8799cc --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/config/VirtualThreadPropertiesBinding.java @@ -0,0 +1,177 @@ +/* + * 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.config; + +import org.apache.hertzbeat.common.concurrent.AdmissionMode; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.ConstructorBinding; +import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.boot.context.properties.bind.Name; + +/** + * Spring Boot binding adapter for {@link VirtualThreadProperties}. + */ +@ConfigurationProperties(prefix = "hertzbeat.vthreads") +public record VirtualThreadPropertiesBinding( + @DefaultValue("true") boolean enabled, + PoolProperties collector, + PoolProperties common, + PoolProperties manager, + AlerterProperties alerter, + PoolProperties warehouse, + AsyncProperties async) { + + @ConstructorBinding + public VirtualThreadPropertiesBinding { + VirtualThreadProperties runtimeProperties = new VirtualThreadProperties(enabled, + toRuntimePool(collector), + toRuntimePool(common), + toRuntimePool(manager), + toRuntimeAlerter(alerter), + toRuntimePool(warehouse), + toRuntimeAsync(async)); + enabled = runtimeProperties.enabled(); + collector = PoolProperties.fromRuntime(runtimeProperties.collector()); + common = PoolProperties.fromRuntime(runtimeProperties.common()); + manager = PoolProperties.fromRuntime(runtimeProperties.manager()); + alerter = AlerterProperties.fromRuntime(runtimeProperties.alerter()); + warehouse = PoolProperties.fromRuntime(runtimeProperties.warehouse()); + async = AsyncProperties.fromRuntime(runtimeProperties.async()); + } + + public VirtualThreadProperties toRuntimeProperties() { + return new VirtualThreadProperties(enabled, + toRuntimePool(collector), + toRuntimePool(common), + toRuntimePool(manager), + toRuntimeAlerter(alerter), + toRuntimePool(warehouse), + toRuntimeAsync(async)); + } + + /** + * Pool-level binding model. + */ + public record PoolProperties( + @DefaultValue("UNBOUNDED_VT") AdmissionMode mode, + @DefaultValue("0") int maxConcurrentJobs) { + + @ConstructorBinding + public PoolProperties { + mode = mode == null ? AdmissionMode.UNBOUNDED_VT : mode; + } + + static PoolProperties fromRuntime(VirtualThreadProperties.PoolProperties runtimeProperties) { + return runtimeProperties == null ? null + : new PoolProperties(runtimeProperties.mode(), runtimeProperties.maxConcurrentJobs()); + } + } + + /** + * Alerter-specific binding model. + */ + public record AlerterProperties( + @Name("notify") PoolProperties notifyPool, + @DefaultValue("10") int periodicMaxConcurrentJobs, + QueueProperties logWorker, + QueueProperties reduce, + QueueProperties windowEvaluator, + @DefaultValue("4") int notifyMaxConcurrentPerChannel) { + + @ConstructorBinding + public AlerterProperties { + } + + static AlerterProperties fromRuntime(VirtualThreadProperties.AlerterProperties runtimeProperties) { + return runtimeProperties == null ? null + : new AlerterProperties( + PoolProperties.fromRuntime(runtimeProperties.notifyPool()), + runtimeProperties.periodicMaxConcurrentJobs(), + QueueProperties.fromRuntime(runtimeProperties.logWorker()), + QueueProperties.fromRuntime(runtimeProperties.reduce()), + QueueProperties.fromRuntime(runtimeProperties.windowEvaluator()), + runtimeProperties.notifyMaxConcurrentPerChannel()); + } + } + + /** + * Queue-preserving binding model. + */ + public record QueueProperties( + @DefaultValue("0") int maxConcurrentJobs, + @DefaultValue("0") int queueCapacity) { + + @ConstructorBinding + public QueueProperties { + } + + static QueueProperties fromRuntime(VirtualThreadProperties.QueueProperties runtimeProperties) { + return runtimeProperties == null ? null + : new QueueProperties(runtimeProperties.maxConcurrentJobs(), runtimeProperties.queueCapacity()); + } + } + + /** + * Async executor binding model. + */ + public record AsyncProperties( + @DefaultValue("true") boolean enabled, + @DefaultValue("256") int concurrencyLimit, + @DefaultValue("true") boolean rejectWhenLimitReached, + @DefaultValue("5000") long taskTerminationTimeout) { + + @ConstructorBinding + public AsyncProperties { + } + + static AsyncProperties fromRuntime(VirtualThreadProperties.AsyncProperties runtimeProperties) { + return runtimeProperties == null ? null + : new AsyncProperties(runtimeProperties.enabled(), runtimeProperties.concurrencyLimit(), + runtimeProperties.rejectWhenLimitReached(), runtimeProperties.taskTerminationTimeout()); + } + } + + private static VirtualThreadProperties.PoolProperties toRuntimePool(PoolProperties poolProperties) { + return poolProperties == null ? null + : new VirtualThreadProperties.PoolProperties(poolProperties.mode(), poolProperties.maxConcurrentJobs()); + } + + private static VirtualThreadProperties.AlerterProperties toRuntimeAlerter(AlerterProperties alerterProperties) { + return alerterProperties == null ? null + : new VirtualThreadProperties.AlerterProperties( + toRuntimePool(alerterProperties.notifyPool()), + alerterProperties.periodicMaxConcurrentJobs(), + toRuntimeQueue(alerterProperties.logWorker()), + toRuntimeQueue(alerterProperties.reduce()), + toRuntimeQueue(alerterProperties.windowEvaluator()), + alerterProperties.notifyMaxConcurrentPerChannel()); + } + + private static VirtualThreadProperties.QueueProperties toRuntimeQueue(QueueProperties queueProperties) { + return queueProperties == null ? null + : new VirtualThreadProperties.QueueProperties(queueProperties.maxConcurrentJobs(), + queueProperties.queueCapacity()); + } + + private static VirtualThreadProperties.AsyncProperties toRuntimeAsync(AsyncProperties asyncProperties) { + return asyncProperties == null ? null + : new VirtualThreadProperties.AsyncProperties(asyncProperties.enabled(), + asyncProperties.concurrencyLimit(), asyncProperties.rejectWhenLimitReached(), + asyncProperties.taskTerminationTimeout()); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatConversation.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatConversation.java new file mode 100644 index 0000000..22e2f94 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatConversation.java @@ -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.common.entity.ai; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * Entity for storing AI chat conversation metadata + */ +@Data +@Builder +@Entity +@EntityListeners(AuditingEntityListener.class) +@Table(name = "hzb_ai_conversation") +@AllArgsConstructor +@NoArgsConstructor +public class ChatConversation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Schema(title = "conversation title") + private String title; + + @Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "The modifier of this record", example = "tom", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + @Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "1612198444000", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; + + /** + * List of messages in this conversation (one-to-many relationship) + */ + @OneToMany(mappedBy = "conversation") + private List<ChatMessage> messages; + + private String securityData; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatMessage.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatMessage.java new file mode 100644 index 0000000..3e7568b --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatMessage.java @@ -0,0 +1,96 @@ +/* + * 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.ai; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Entity for storing individual chat messages in conversations + */ +@Data +@Builder +@Entity +@EntityListeners(AuditingEntityListener.class) +@Table(name = "hzb_ai_message", indexes = { + @Index(name = "idx_message_conversation_id", columnList = "conversation_id") +}) +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Schema(title = "conversation id") + @Column(name = "conversation_id", insertable = false, updatable = false) + private Long conversationId; + + @Schema(title = "conversation", hidden = true) + @ManyToOne + @JoinColumn(name = "conversation_id") + private ChatConversation conversation; + + @Schema(title = "message content") + @Lob + @NotBlank + private String content; + + @Schema(title = "message role: user, system") + private String role; + + @Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "The modifier of this record", example = "tom", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + @Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "1612198444000", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; + +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/ai/SopSchedule.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/ai/SopSchedule.java new file mode 100644 index 0000000..d0efbfb --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/ai/SopSchedule.java @@ -0,0 +1,111 @@ +/* + * 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.ai; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Entity for storing scheduled SOP execution configurations. + * Allows users to schedule automatic SOP executions with results pushed to conversations. + */ +@Data +@Builder +@Entity +@EntityListeners(AuditingEntityListener.class) +@Table(name = "hzb_sop_schedule", indexes = { + @Index(name = "idx_schedule_conversation_id", columnList = "conversation_id"), + @Index(name = "idx_schedule_enabled_next", columnList = "enabled, next_run_time") +}) +@AllArgsConstructor +@NoArgsConstructor +public class SopSchedule { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Schema(title = "Conversation ID to push results to") + @NotNull + @Column(name = "conversation_id") + private Long conversationId; + + @Schema(title = "Name of the SOP skill to execute") + @NotBlank + @Column(name = "sop_name", length = 64) + private String sopName; + + @Schema(title = "SOP execution parameters in JSON format") + @Column(name = "sop_params", length = 1024) + private String sopParams; + + @Schema(title = "Cron expression for scheduling") + @NotBlank + @Column(name = "cron_expression", length = 64) + private String cronExpression; + + @Schema(title = "Whether the schedule is enabled") + @Builder.Default + @Column(name = "enabled") + private Boolean enabled = true; + + @Schema(title = "Last execution time") + @Column(name = "last_run_time") + private LocalDateTime lastRunTime; + + @Schema(title = "Next scheduled execution time") + @Column(name = "next_run_time") + private LocalDateTime nextRunTime; + + @Schema(title = "The creator of this record", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "The modifier of this record", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + @Schema(title = "Record create time", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertDefine.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertDefine.java new file mode 100644 index 0000000..a0dd83c --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertDefine.java @@ -0,0 +1,121 @@ +/* + * 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 static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Alarm Define Rule Entity + */ +@Entity +@Table(name = "hzb_alert_define") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Alarm Threshold Entity") +@EntityListeners(AuditingEntityListener.class) +public class AlertDefine { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Threshold Id", example = "87584674384", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "Alert Rule Name", example = "high_cpu_usage", accessMode = READ_WRITE) + @Size(max = 100) + @NotNull + private String name; + + @Schema(title = "Rule Type: realtime_metric, periodic_metric, realtime_log, periodic_log", example = "realtime_metric") + private String type; + + @Schema(title = "Alarm Threshold Expr", example = "usage>90", accessMode = READ_WRITE) + @Size(max = 2048) + @Column(length = 2048) + private String expr; + + @Schema(title = "Execution Period/ Window Size (seconds) - For periodic rules/ For log realtime", example = "300") + private Integer period; + + @Schema(title = "Alarm Trigger Times.The alarm is triggered only after the required number of times is reached", + example = "3", accessMode = READ_WRITE) + private Integer times; + + @Schema(description = "labels(status:success,env:prod,priority:critical)", example = "{name: key1, value: value1}", + accessMode = READ_WRITE) + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 2048) + private Map<String, String> labels; + + @Schema(title = "Annotations", example = "summary: High CPU usage") + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 4096) + private Map<String, String> annotations; + + @Schema(title = "Alert Content Template", example = "Instance {{ $labels.instance }} CPU usage is {{ $value }}%") + @Size(max = 2048) + @Column(length = 2048) + private String template; + + @Schema(title = "Data Source Type", example = "PROMETHEUS") + @Size(max = 100) + private String datasource; + + @Schema(title = "Is Enabled", example = "true") + private boolean enable = true; + + @Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "The modifier of this record", example = "tom", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + @Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "1612198444000", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertDefineMonitorBind.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertDefineMonitorBind.java new file mode 100644 index 0000000..f40241c --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertDefineMonitorBind.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.alerter; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Alarm Threshold Relate Monitor Entity + */ +@Entity +@Table(name = "hzb_alert_define_monitor_bind", indexes = { + @Index(name = "idx_alert_define_id", columnList = "alert_define_id"), + @Index(name = "idx_monitor_id", columnList = "monitor_id") +}) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Alarm Threshold Relate Monitor Entity") +@EntityListeners(AuditingEntityListener.class) +public class AlertDefineMonitorBind { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "id", example = "74384", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "Alarm Define Id", example = "87432674384", accessMode = READ_WRITE) + private Long alertDefineId; + + @Schema(title = "Monitor Id", example = "87432674336", accessMode = READ_WRITE) + @Column(name = "monitor_id") + private Long monitorId; + + @Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "1612198444000", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; + + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "monitor_id", referencedColumnName = "id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), + insertable = false, updatable = false) + // todo instead of @NotFound + // @NotFound(action = NotFoundAction.IGNORE) + private Monitor monitor; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertGroupConverge.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertGroupConverge.java new file mode 100644 index 0000000..0debf14 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertGroupConverge.java @@ -0,0 +1,106 @@ +/* + * 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; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.JsonStringListAttributeConverter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Alert group converge strategy entity + */ +@Entity +@Table(name = "hzb_alert_group_converge", indexes = { + @Index(name = "idx_name", columnList = "name") +}) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Alert Group Converge Policy Entity") +@EntityListeners(AuditingEntityListener.class) +public class AlertGroupConverge { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Primary Key Index ID", example = "87584674384") + private Long id; + + @Schema(title = "Policy name", example = "group-converge-1") + @Size(max = 100) + @NotNull + @Column(name = "name") + private String name; + + @Schema(title = "Labels to group by", example = "[\"instance\"]") + @Convert(converter = JsonStringListAttributeConverter.class) + @Column(name = "group_labels", length = 1024) + private List<String> groupLabels; + + @Schema(title = "Initial wait time before sending first group alert (s)", example = "30") + @Column(name = "group_wait") + private Long groupWait; + + @Schema(title = "Interval between group alert sends (s)", example = "300") + @Column(name = "group_interval") + private Long groupInterval; + + @Schema(title = "Interval for repeating firing alerts (s), set to 0 to disable repeating", example = "9000") + @Column(name = "repeat_interval") + private Long repeatInterval; + + @Schema(title = "Whether to enable this policy", example = "true") + private Boolean enable; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by", example = "tom") + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)") + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertInhibit.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertInhibit.java new file mode 100644 index 0000000..537f09d --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertInhibit.java @@ -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. + */ + +/* + * 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; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.JsonStringListAttributeConverter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * alert inhibit rule + */ +@Entity +@Table(name = "hzb_alert_inhibit") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Alert Inhibit Rule Entity") +@EntityListeners(AuditingEntityListener.class) +public class AlertInhibit { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Inhibit Rule ID", example = "1") + private Long id; + + @Schema(title = "Inhibit Rule Name", example = "inhibit_high_cpu") + @Size(max = 100) + @NotNull + private String name; + + /** + * Source alert labels that must match for the inhibit rule to be active + * Example: severity=critical,instance=web-01 + */ + @Schema(title = "Source Alert Match Labels") + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 2048) + private Map<String, String> sourceLabels; + + /** + * Target alert labels that must match to be inhibited + * Example: severity=warning,instance=web-01 + */ + @Schema(title = "Target Alert Match Labels") + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 2048) + private Map<String, String> targetLabels; + + /** + * Labels that must have the same value in the source and target alert for the inhibit rule to be active + * Example: ["instance", "job"] + */ + @Schema(title = "Equal Labels") + @Convert(converter = JsonStringListAttributeConverter.class) + @Column(length = 2048) + private List<String> equalLabels; + + @Schema(title = "Whether to enable this policy", example = "true") + private Boolean enable; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by", example = "tom") + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)") + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertSilence.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertSilence.java new file mode 100644 index 0000000..c9795bf --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertSilence.java @@ -0,0 +1,118 @@ +/* + * 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; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.JsonByteListAttributeConverter; +import org.apache.hertzbeat.common.entity.manager.ZonedDateTimeAttributeConverter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Alert Silence strategy entity + */ +@Entity +@Table(name = "hzb_alert_silence") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Alert Silence Policy Entity") +@EntityListeners(AuditingEntityListener.class) +public class AlertSilence { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Primary Key Index ID", + example = "87584674384") + private Long id; + + @Schema(title = "Policy name", example = "silence-1") + @Size(max = 100) + @NotNull + private String name; + + @Schema(title = "Whether to enable this policy", example = "true") + private boolean enable = true; + + @Schema(title = "Whether to match all", example = "true") + private boolean matchAll = true; + + @Schema(title = "Silence type 0: once, 1:cyc", example = "1") + @NotNull + private Byte type; + + @Schema(title = "Silenced alerts num", example = "3") + private Integer times; + + @Schema(description = "Match the alarm information label", example = "{name: key1, value: value1}") + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 2048) + private Map<String, String> labels; + + @Schema(title = "The day of the WEEK is valid in periodic silence, multiple," + + " all or empty is daily 7: Sunday 1: Monday 2: Tuesday 3: Wednesday 4: Thursday 5: Friday 6: Saturday", + example = "[0,1]") + @Convert(converter = JsonByteListAttributeConverter.class) + private List<Byte> days; + + @Schema(title = "Limit time period start", example = "00:00:00") + @Convert(converter = ZonedDateTimeAttributeConverter.class) + private ZonedDateTime periodStart; + + @Schema(title = "Restricted time period end", example = "23:59:59") + @Convert(converter = ZonedDateTimeAttributeConverter.class) + private ZonedDateTime periodEnd; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by", example = "tom") + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)") + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/GroupAlert.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/GroupAlert.java new file mode 100644 index 0000000..84fb7b3 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/GroupAlert.java @@ -0,0 +1,112 @@ +/* + * 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 static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.JsonStringListAttributeConverter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Group Alert Content Entity + */ +@Entity +@Table(name = "hzb_alert_group", indexes = {@Index(name = "unique_group_key", columnList = "group_key", unique = true)}) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Group Alarm Content Entity") +@EntityListeners(AuditingEntityListener.class) +public class GroupAlert { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Threshold Id", example = "87584674384", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "Group Key", example = "HighCPUUsage{alertname=\"HighCPUUsage\", instance=\"server1\"}") + @Column(length = 2048) + private String groupKey; + + @Schema(title = "Status", example = "resolved") + private String status; + + @Schema(title = "Group Labels", example = "{\"alertname\": \"HighCPUUsage\"}") + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 2048) + private Map<String, String> groupLabels; + + @Schema(title = "Common Labels", example = "{\"alertname\": \"HighCPUUsage\", \"instance\": \"server1\", \"severity\": \"critical\"}") + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 2048) + private Map<String, String> commonLabels; + + @Schema(title = "Common Annotations", example = "{\"summary\": \"High CPU usage detected\", \"description\": \"CPU usage is back to normal for server1\"}") + @Convert(converter = JsonMapAttributeConverter.class) + @Column(columnDefinition = "TEXT") + private Map<String, String> commonAnnotations; + + @Schema(title = "Alert Fingerprints", example = "[\"dxsdfdsf\"]") + @Convert(converter = JsonStringListAttributeConverter.class) + @Column(columnDefinition = "TEXT") + private List<String> alertFingerprints; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by", example = "tom") + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)") + @CreatedDate + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)") + @LastModifiedDate + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime gmtUpdate; + + @Transient + private List<SingleAlert> alerts; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/JsonMapAttributeConverter.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/JsonMapAttributeConverter.java new file mode 100644 index 0000000..f5ecf73 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/JsonMapAttributeConverter.java @@ -0,0 +1,47 @@ +/* + * 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 tools.jackson.core.type.TypeReference; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.stereotype.Component; + +/** + * json map converter + */ +@Converter +@Component +public class JsonMapAttributeConverter implements AttributeConverter<Map<String, String>, String> { + + @Override + public String convertToDatabaseColumn(Map<String, String> attribute) { + return JsonUtil.toJson(attribute); + } + + @Override + public Map<String, String> convertToEntityAttribute(String dbData) { + TypeReference<Map<String, String>> typeReference = new TypeReference<>() {}; + Map<String, String> map = JsonUtil.fromJson(dbData, typeReference); + return Objects.requireNonNullElseGet(map, HashMap::new); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java new file mode 100644 index 0000000..cc7dcd6 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeReceiver.java @@ -0,0 +1,301 @@ +/* + * 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 static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Message notification recipient entity + */ +@Entity +@Table(name = "hzb_notice_receiver") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Message notification recipient entity") +@EntityListeners(AuditingEntityListener.class) +public class NoticeReceiver { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Recipient entity primary key index ID", description = "Recipient entity primary key index ID", + example = "87584674384", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "Recipient name", description = "Recipient name", + example = "tom", accessMode = READ_WRITE) + @Size(max = 100) + @NotBlank(message = "name can not null") + private String name; + + @Schema(title = "Notification information method: 0-SMS 1-Email 2-webhook 3-WeChat Official Account 4-Enterprise WeChat Robot " + + "5-DingTalk Robot 6-FeiShu Robot 7-Telegram Bot 8-SlackWebHook 9-Discord Bot 10-Enterprise WeChat app message " + + "11-Huawei Cloud SMN 12-ServerChan 13-Gotify 14-FeiShu app message 15-Ntfy", + description = "Notification information method: " + + "0-SMS 1-Email 2-webhook 3-WeChat Official Account " + + "4-Enterprise WeChat Robot 5-DingTalk Robot 6-FeiShu Robot " + + "7-Telegram Bot 8-SlackWebHook 9-Discord Bot 10-Enterprise " + + "WeChat app message 11-Huawei Cloud SMN 12-ServerChan 13-Gotify 14-FeiShu app message 15-Ntfy", + accessMode = READ_WRITE) + @Min(0) + @NotNull(message = "type can not null") + private Byte type; + + @Schema(title = "Mobile number: Valid when the notification method is SMS", + description = "Mobile number: Valid when the notification method is SMS", + example = "18923435643", accessMode = READ_WRITE) + @Size(max = 100) + private String phone; + + @Schema(title = "Email account: Valid when the notification method is email", + description = "Email account: Valid when the notification method is email", + example = "tom@qq.com", accessMode = READ_WRITE) + @Size(max = 100) + private String email; + + @Schema(title = "URL address: The notification method is valid for webhook", + description = "URL address: The notification method is valid for webhook", + example = "https://www.tancloud.cn", accessMode = READ_WRITE) + @Size(max = 1000) + @Column(length = 1000) + private String hookUrl; + + @Schema(title = "Auth Type: WebHook Authorization Type", + description = "Auth Type: WebHook Authorization Type, one of the 'None', 'Basic' and 'Bearer'", + example = "None", accessMode = READ_ONLY) + @Size(max = 300) + @Column(length = 300) + private String hookAuthType; + + @Schema(title = "Auth Token: WebHook Authorization Token", + description = "Auth Token: WebHook Authorization Token", + example = "YWxpY2U6c3VwZXJtYW4", accessMode = READ_WRITE) + @Size(max = 300) + @Column(length = 300) + private String hookAuthToken; + + @Schema(title = "openId : The notification method is valid for WeChat official account, enterprise WeChat robot or FlyBook robot", + description = "openId : The notification method is valid for WeChat official account, enterprise WeChat robot or FlyBook robot", + example = "343432", accessMode = READ_WRITE) + @Size(max = 300) + @Column(length = 300) + private String wechatId; + + @Schema(title = "FeiShu app id : The notification method is valid for FeiShu app message", + description = "FeiShu app id : The notification method is valid for FeiShu app message", + example = "34823984635647", accessMode = READ_WRITE) + @Size(max = 255) + private String appId; + + @Schema(title = "Access token : The notification method is valid for DingTalk robot", + description = "Access token : The notification method is valid for DingTalk robot", + example = "34823984635647", accessMode = READ_WRITE) + @Size(max = 300) + @Column(length = 300) + private String accessToken; + + @Schema(title = "Telegram bot token : The notification method is valid for Telegram Bot", + description = "Telegram bot token : The notification method is valid for Telegram Bot", + example = "1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJMXXXXXX", accessMode = READ_WRITE) + private String tgBotToken; + + @Schema(title = "Telegram user id: The notification method is valid for Telegram Bot", + description = "Telegram user id: The notification method is valid for Telegram Bot", + example = "779294123", accessMode = READ_WRITE) + private String tgUserId; + + @Schema(title = "Telegram message thread id: The notification method is valid for Telegram Bot", + description = "TTelegram message thread id: The notification method is valid for Telegram Bot", + example = "779294123", accessMode = READ_WRITE) + private String tgMessageThreadId; + + @Schema(title = "FeiShu app message receiveType: 0-user 1-chat 2-party 3-all", + description = "FeiShu app message receiveType: 0-user 1-chat 2-party 3-all", + example = "1", accessMode = READ_WRITE) + private Byte larkReceiveType; + + @Schema(title = "DingTalk,FeiShu,WeWork user id: The notification method is valid for DingTalk,FeiShu,WeWork Bot", + description = "DingTalk,FeiShu,WeWork user id: The notification method is valid for DingTalk,FeiShu,WeWork Bot", + example = "779294123", accessMode = READ_WRITE) + private String userId; + + @Schema(title = "FeiShu app message chatId: The notification method is valid for FeiShu app message", + description = "FeiShu app message chatId: The notification method is valid for FeiShu app message", + example = "779294123", accessMode = READ_WRITE) + private String chatId; + + @Schema(title = "URL address: The notification method is valid for Slack", + description = "URL address: The notification method is valid for Slack", + example = "https://hooks.slack.com/services/XXXX/XXXX/XXXX", accessMode = READ_WRITE) + @Size(max = 300) + @Column(length = 300) + private String slackWebHookUrl; + + @Schema(title = "Enterprise weChat message: The notification method is valid for Enterprise WeChat app message", + description = "Enterprise weChat message: The notification method is valid for Enterprise WeChat app message", + example = "ww1a603432123d0dc1", accessMode = READ_WRITE) + private String corpId; + + @Schema(title = "Enterprise weChat appId: The notification method is valid for Enterprise WeChat app message", + description = "Enterprise weChat appId: The notification method is valid for Enterprise WeChat app message", + example = "1000001", accessMode = READ_WRITE) + private Integer agentId; + + @Schema(title = "Enterprise weChat secret: The notification method is valid for Enterprise WeChat app message", + description = "Enterprise weChat secret: The notification method is valid for Enterprise WeChat app message", + example = "oUydwn92ey0lnuY02MixNa57eNK-20dJn5NEOG-u2uE", accessMode = READ_WRITE) + private String appSecret; + + @Schema(title = "Enterprise weChat party id: The notification method is valid for Enterprise WeChat app message", + description = "Enterprise weChat party id: The notification method is valid for Enterprise WeChat app message", + example = "779294123", accessMode = READ_WRITE) + private String partyId; + + @Schema(title = "Enterprise weChat tag id: The notification method is valid for Enterprise WeChat app message", + description = "Enterprise weChat tag id: The notification method is valid for Enterprise WeChat app message", + example = "779294123", accessMode = READ_WRITE) + private String tagId; + + @Schema(title = "Discord channel id: The notification method is valid for Discord", + description = "Discord channel id: The notification method is valid for Discord", + example = "1065303416030642266", accessMode = READ_WRITE) + @Size(max = 300) + @Column(length = 300) + private String discordChannelId; + + @Schema(title = "Discord bot token: The notification method is valid for Discord", + description = "Discord bot token: The notification method is valid for Discord", + example = "MTA2NTMwMzU0ODY4Mzg4MjUzNw.xxxxx.xxxxxxx", accessMode = READ_WRITE) + @Size(max = 300) + @Column(length = 300) + private String discordBotToken; + + @Schema(title = "huawei cloud SMN ak: If the notification method is valid for huawei cloud SMN", + description = "huawei cloud SMN ak: If the notification method is valid for huawei cloud SMN", + example = "NCVBODJOEYHSW3VNXXXX", accessMode = READ_WRITE) + @Size(max = 22) + @Column(length = 22) + private String smnAk; + + @Schema(title = "huawei cloud SMN sk: If the notification method is valid for huawei cloud SMN", + description = "huawei cloud SMN sk: If the notification method is valid for huawei cloud SMN", + example = "nmSNhUJN9MlpPl8lfCsgdA0KvHCL9JXXXX", accessMode = READ_WRITE) + @Size(max = 42) + @Column(length = 42) + private String smnSk; + + @Schema(title = "huawei cloud SMN projectId: If the notification method is valid for huawei cloud SMN", + description = "huawei cloud SMN projectId: If the notification method is valid for huawei cloud SMN", + example = "320c2fb11edb47a481c299c1XXXXXX", accessMode = READ_WRITE) + @Size(max = 32) + @Column(length = 32) + private String smnProjectId; + + @Schema(title = "huawei cloud SMN region: If the notification method is valid for huawei cloud SMN", + description = "huawei cloud SMN region: If the notification method is valid for huawei cloud SMN", + example = "cn-east-3", accessMode = READ_WRITE) + @Size(max = 32) + @Column(length = 32) + private String smnRegion; + + @Schema(title = "huawei cloud SMN TopicUrn: If the notification method is valid for huawei cloud SMN", + description = "huawei cloud SMN TopicUrn: If the notification method is valid for huawei cloud SMN", + example = "urn:smn:cn-east-3:xxx:hertzbeat_test", accessMode = READ_WRITE) + @Size(max = 300) + @Column(length = 300) + private String smnTopicUrn; + + @Schema(title = "serverChanToken : The notification method is valid for ServerChan", + description = "serverChanToken : The notification method is valid for ServerChan", + example = "SCT193569TSNm6xIabdjqeZPtOGOWcvU1e", accessMode = READ_WRITE) + @Size(max = 300) + @Column(length = 300) + private String serverChanToken; + + @Schema(title = "Gotify token : The notification method is valid for Gotify", + description = "Gotify token : The notification method is valid for Gotify", + example = "A845h__ZMqDxZlO", accessMode = READ_WRITE) + @Size(max = 300) + @Column(length = 300) + private String gotifyToken; + + @Schema(title = "Ntfy server URL : The notification method is valid for Ntfy", + description = "Ntfy server URL : The notification method is valid for Ntfy, default is https://ntfy.sh", + example = "https://ntfy.sh", accessMode = READ_WRITE) + @Size(max = 300) + @Column(length = 300) + private String ntfyServerUrl; + + @Schema(title = "Ntfy topic : The notification method is valid for Ntfy", + description = "Ntfy topic : The notification method is valid for Ntfy", + example = "hertzbeat-alerts", accessMode = READ_WRITE) + @Size(max = 300) + @Column(length = 300) + private String ntfyTopic; + + @Schema(title = "Ntfy access token : Bearer token for self-hosted ntfy servers with authentication", + description = "Ntfy access token : Bearer token for self-hosted ntfy servers with authentication", + example = "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", accessMode = READ_WRITE) + @Size(max = 300) + @Column(length = 300) + private String ntfyToken; + + @Schema(title = "The creator of this record", example = "tom", + accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by", example = "tom", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + @Schema(title = "Record creation time (millisecond timestamp)", + example = "1612198922000", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)", + example = "1612198444000", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; + +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeRule.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeRule.java new file mode 100644 index 0000000..439f044 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeRule.java @@ -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.common.entity.alerter; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.JsonByteListAttributeConverter; +import org.apache.hertzbeat.common.entity.manager.JsonLongListAttributeConverter; +import org.apache.hertzbeat.common.entity.manager.JsonStringListAttributeConverter; +import org.apache.hertzbeat.common.entity.manager.ZonedDateTimeAttributeConverter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Notification strategy entity + */ +@Entity +@Table(name = "hzb_notice_rule") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Notify Policy Entity") +@EntityListeners(AuditingEntityListener.class) +public class NoticeRule { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Notification Policy Entity Primary Key Index ID", + description = "Notification Policy Entity Primary Key Index ID", + example = "87584674384", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "Policy name", + description = "Policy name", + example = "dispatch-1", accessMode = READ_WRITE) + @Size(max = 100) + @NotBlank(message = "name can not null") + private String name; + + @Schema(title = "Recipient ID", + description = "Recipient ID", + example = "4324324", accessMode = READ_WRITE) + @NotEmpty(message = "receiverId can not empty") + @Convert(converter = JsonLongListAttributeConverter.class) + private List<Long> receiverId; + + @Schema(title = "Recipient identification", + description = "Recipient identification", + example = "tom", accessMode = READ_WRITE) + @Convert(converter = JsonStringListAttributeConverter.class) + private List<String> receiverName; + + @Schema(title = "Template ID", + description = "Template ID", + example = "4324324", accessMode = READ_WRITE) + private Long templateId; + + @Schema(title = "Template identification", + description = "Template identification", + example = "demo", accessMode = READ_WRITE) + @Size(max = 100) + private String templateName; + + @Schema(title = "Whether to enable this policy", + description = "Whether to enable this policy", + example = "true", accessMode = READ_WRITE) + private boolean enable = true; + + @Schema(title = "Whether to forward all", + description = "Whether to forward all", + example = "false", accessMode = READ_WRITE) + private boolean filterAll = true; + + @Schema(title = "Labels", example = "{\"alertname\": \"HighCPUUsage\", \"priority\": \"critical\", \"instance\": \"343483943\"}") + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 2048) + private Map<String, String> labels; + + @Schema(title = "Day of the week, multiple, all or empty is daily 7: Sunday 1: Monday 2: Tuesday 3: Wednesday 4: Thursday 5: Friday 6: Saturday", + example = "[0,1]", accessMode = READ_WRITE) + @Convert(converter = JsonByteListAttributeConverter.class) + private List<Byte> days; + + @Schema(title = "Limit time period start", example = "00:00:00", accessMode = READ_WRITE) + @Convert(converter = ZonedDateTimeAttributeConverter.class) + private ZonedDateTime periodStart; + + @Schema(title = "Restricted time period end", example = "23:59:59", accessMode = READ_WRITE) + @Convert(converter = ZonedDateTimeAttributeConverter.class) + private ZonedDateTime periodEnd; + + @Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by", example = "tom", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeTemplate.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeTemplate.java new file mode 100644 index 0000000..fcf78db --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/NoticeTemplate.java @@ -0,0 +1,121 @@ +/* + * 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 static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Notification template entity + */ +@Entity +@Table(name = "hzb_notice_template") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Notify Policy Template") +@EntityListeners(AuditingEntityListener.class) +public class NoticeTemplate { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Notification Template Entity Primary Key Index ID", + description = "Notification Template Entity Primary Key Index ID", + example = "87584674384", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "Template name", + description = "Template name", + example = "dispatch-1", accessMode = READ_WRITE) + @Size(max = 100) + @NotBlank + private String name; + + @Schema(title = "Notification information method: 0-SMS 1-Email 2-webhook 3-WeChat Official Account 4-Enterprise WeChat Robot " + + "5-DingTalk Robot 6-FeiShu Robot 7-Telegram Bot 8-SlackWebHook 9-Discord Bot 10-Enterprise WeChat app message", + description = "Notification information method: " + + "0-SMS 1-Email 2-webhook 3-WeChat Official Account " + + "4-Enterprise WeChat Robot 5-DingTalk Robot 6-FeiShu Robot " + + "7-Telegram Bot 8-SlackWebHook 9-Discord Bot 10-Enterprise WeChat app message", + accessMode = READ_WRITE) + @Min(0) + @NotNull + private Byte type; + + @Schema(title = "Is it a preset template: true- preset template false- custom template.", + description = "Is it a preset template: true- preset template false- custom template.", + accessMode = READ_WRITE) + @Column(columnDefinition = "boolean default false") + private boolean preset = false; + + @Schema(title = "Template content", + description = "Template content", + example = """ + [${title}] + ${targetLabel} : ${target} + <#if (monitorId??)>${monitorIdLabel} : ${monitorId} </#if> + <#if (monitorName??)>${monitorNameLabel} : ${monitorName} </#if> + <#if (monitorHost??)>${monitorHostLabel} : ${monitorHost} </#if> + ${priorityLabel} : ${priority} + ${triggerTimeLabel} : ${triggerTime} + ${contentLabel} : ${content}""", accessMode = READ_WRITE) + @Size(max = 60000) + @Lob + @NotBlank + private String content; + + @Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by", example = "tom", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/SingleAlert.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/SingleAlert.java new file mode 100644 index 0000000..617655c --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/SingleAlert.java @@ -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.common.entity.alerter; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Single Alert Content Entity + */ +@Entity +@Table(name = "hzb_alert_single", indexes = {@Index(name = "unique_fingerprint", columnList = "fingerprint", unique = true)}) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Single Alarm Content Entity") +@EntityListeners(AuditingEntityListener.class) +public class SingleAlert { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Threshold Id", example = "87584674384", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "Fingerprint", example = "alertname:demo") + @Column(length = 2048) + private String fingerprint; + + @Schema(title = "Labels", example = "{\"alertname\": \"HighCPUUsage\", \"priority\": \"critical\", \"instance\": \"343483943\"}") + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 2048) + private Map<String, String> labels; + + @Schema(title = "Annotations", example = "{\"summary\": \"High CPU usage detected\"}") + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 4096) + private Map<String, String> annotations; + + @Schema(title = "Content", example = "CPU usage is above 80% for the last 5 minutes on instance server1.example.com.") + @Column(length = 4096) + private String content; + + @Schema(title = "Status", example = "firing|resolved") + private String status; + + @Schema(title = "Trigger Times", example = "1") + private Integer triggerTimes; + + @Schema(title = "Start At", example = "1734005477630") + private Long startAt; + + @Schema(title = "Active At", example = "1734005477630") + private Long activeAt; + + @Schema(title = "End At, when status is resolved has", example = "null") + private Long endAt; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by", example = "tom") + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)") + @CreatedDate + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)") + @LastModifiedDate + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime gmtUpdate; + + @Override + public SingleAlert clone() { + // deep clone + return JsonUtil.fromJson(JsonUtil.toJson(this), SingleAlert.class); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/grafana/GrafanaDashboard.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/grafana/GrafanaDashboard.java new file mode 100644 index 0000000..2602a4d --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/grafana/GrafanaDashboard.java @@ -0,0 +1,71 @@ +/* + * 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.grafana; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Grafana dashboard entity + */ +@Entity +@Table(name = "hzb_grafana_dashboard") +@Data +@Builder(toBuilder = true) +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Grafana dashboard entity") +public class GrafanaDashboard implements Serializable { + + @Id + @Schema(description = "Monitor id") + private Long monitorId; + + @Schema(description = "Dashboard folderUid") + private String folderUid; + + @Schema(description = "Dashboard slug") + private String slug; + + @Schema(description = "Dashboard status") + private String status; + + @Schema(description = "Dashboard uid") + private String uid; + + @Schema(description = "Dashboard url") + private String url; + + @Schema(description = "Dashboard version") + private Long version; + + @Schema(description = "is enabled") + private boolean enabled; + + @Schema(description = "template") + @Transient + private String template; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/AuthToken.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/AuthToken.java new file mode 100644 index 0000000..1845c36 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/AuthToken.java @@ -0,0 +1,94 @@ +/* + * 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.manager; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; + +/** + * API Token Entity - stores generated non-expiring API tokens for management and revocation. + * <p> + * Only "managed" tokens (those generated after this feature was introduced) are stored here. + * Legacy tokens are not tracked and continue to work without DB validation. + * </p> + */ +@Entity +@Table(name = "hzb_auth_token") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "API Token entity") +@EntityListeners(AuditingEntityListener.class) +public class AuthToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "primary id", example = "1", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "Token name/description", example = "Alert integration token", accessMode = READ_WRITE) + @Column(length = 255) + private String name; + + @Schema(title = "SHA-256 hash of the token value", accessMode = READ_ONLY) + @Column(length = 128, nullable = false, unique = true) + private String tokenHash; + + @Schema(title = "Masked token for display", example = "eyJh****xMjM", accessMode = READ_ONLY) + @Column(length = 64) + private String tokenMask; + + @Schema(title = "Token status: 0-active", accessMode = READ_ONLY) + @Column(nullable = false) + @Builder.Default + private Byte status = 0; + + @Schema(title = "The creator of this token", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "Token creation time", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Token expiration time, null means never expire", accessMode = READ_ONLY) + private LocalDateTime expireTime; + + @Schema(title = "Token last used time", accessMode = READ_ONLY) + private LocalDateTime lastUsedTime; + +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Bulletin.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Bulletin.java new file mode 100644 index 0000000..b980822 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Bulletin.java @@ -0,0 +1,92 @@ +/* + * 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.manager; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Bulletin + */ +@Entity +@Data +@Schema(description = "Bulletin") +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +@Table(name = "hzb_bulletin") +public class Bulletin { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Bulletin ID", example = "1") + private Long id; + + @Schema(description = "Bulletin Name", example = "Bulletin1", accessMode = READ_WRITE) + private String name; + + @Schema(description = "Monitor IDs", example = "1") + @Column(name = "monitor_ids", length = 4096) + @Convert(converter = JsonLongListAttributeConverter.class) + private List<Long> monitorIds; + + @Schema(description = "Monitor Type eg: jvm, tomcat", example = "jvm", accessMode = READ_WRITE) + private String app; + + @Schema(description = "Monitor Fields") + @Column(name = "fields", length = 4096) + @Convert(converter = JsonMapListAttributeConverter.class) + private Map<String, List<String>> fields; + + @Schema(title = "The creator of this record", example = "tom", accessMode = READ_WRITE) + @CreatedBy + private String creator; + + @Schema(title = "The modifier of this record", example = "tom", accessMode = READ_WRITE) + @LastModifiedBy + private String modifier; + + @Schema(title = "Record create time", example = "2024-07-02T20:09:34.903217", accessMode = READ_WRITE) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "2024-07-02T20:09:34.903217", accessMode = READ_WRITE) + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Collector.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Collector.java new file mode 100644 index 0000000..c692bab --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Collector.java @@ -0,0 +1,92 @@ +/* + * 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.manager; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * collector entity + */ +@Entity +@Table(name = "hzb_collector", uniqueConstraints = @UniqueConstraint(columnNames = {"name"})) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "slave collector entity") +@EntityListeners(AuditingEntityListener.class) +public class Collector { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "primary id", example = "2") + private Long id; + + @Schema(title = "collector identity name", description = "collector identity name") + @NotBlank(message = "name can not null") + private String name; + + @Schema(title = "collector ip", description = "collector remote ip") + @NotBlank(message = "ip can not null") + private String ip; + + @Schema(title = "collector version", description = "collector version") + private String version; + + @Schema(title = "collector status: 0-online 1-offline") + @Min(0) + private byte status; + + @Schema(title = "collector mode: public or private") + private String mode; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by") + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)") + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/CollectorMonitorBind.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/CollectorMonitorBind.java new file mode 100644 index 0000000..bad92c4 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/CollectorMonitorBind.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.manager; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * collector entity + */ +@Entity +@Table(name = "hzb_collector_monitor_bind", indexes = { + @Index(name = "idx_collector_monitor_collector", columnList = "collector"), + @Index(name = "idx_collector_monitor_monitor_id", columnList = "monitor_id") +}) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "slave collector monitor bind entity") +@EntityListeners(AuditingEntityListener.class) +public class CollectorMonitorBind { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "primary id", example = "23") + private Long id; + + @Schema(title = "collector name", example = "87432674384") + private String collector; + + @Schema(title = "monitor ID", example = "87432674336") + @Column(name = "monitor_id") + private Long monitorId; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by") + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)") + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Define.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Define.java new file mode 100644 index 0000000..a06fca5 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Define.java @@ -0,0 +1,73 @@ +/* + * 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.manager; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * monitor define entity + */ +@Entity +@Table(name = "hzb_define") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "monitor define entity") +@EntityListeners(AuditingEntityListener.class) +public class Define { + + @Id + @Schema(title = "app", example = "websocket") + private String app; + + @Lob + @Schema(title = "define content", description = "define yml content") + private String content; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by") + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)") + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/GeneralConfig.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/GeneralConfig.java new file mode 100644 index 0000000..08325e7 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/GeneralConfig.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.manager; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Common Config Entity + */ +@Entity +@Table(name = "hzb_config") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Common server config entity") +@EntityListeners(AuditingEntityListener.class) +public class GeneralConfig { + + @Id + @Schema(title = "Config type: email sms, primary key ", description = "Config type: email sms, primary key ", + accessMode = READ_WRITE) + @NotBlank(message = "ip can not null") + private String type; + + @Schema(title = "Config content", description = "Config content,formatjson", accessMode = READ_WRITE) + @Column(length = 8192) + private String content; + + @Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by", example = "tom", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonByteListAttributeConverter.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonByteListAttributeConverter.java new file mode 100644 index 0000000..f5b68cc --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonByteListAttributeConverter.java @@ -0,0 +1,44 @@ +/* + * 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.manager; + +import tools.jackson.core.type.TypeReference; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.List; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.stereotype.Component; + +/** + * json str to list Byte converter + */ +@Converter +@Component +public class JsonByteListAttributeConverter implements AttributeConverter<List<Byte>, String> { + + @Override + public String convertToDatabaseColumn(List<Byte> attribute) { + return JsonUtil.toJson(attribute); + } + + @Override + public List<Byte> convertToEntityAttribute(String dbData) { + TypeReference<List<Byte>> typeReference = new TypeReference<>() {}; + return JsonUtil.fromJson(dbData, typeReference); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonLongListAttributeConverter.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonLongListAttributeConverter.java new file mode 100644 index 0000000..6717e79 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonLongListAttributeConverter.java @@ -0,0 +1,61 @@ +/* + * 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.manager; + +import tools.jackson.core.type.TypeReference; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.stereotype.Component; + +/** + * json str to id list + */ +@Converter +@Component +public class JsonLongListAttributeConverter implements AttributeConverter<List<Long>, String> { + @Override + public String convertToDatabaseColumn(List<Long> attribute) { + return JsonUtil.toJson(attribute); + + } + + @Override + public List<Long> convertToEntityAttribute(String dbData) { + if (StringUtils.isBlank(dbData)) { + return List.of(); + } + if (!JsonUtil.isJsonStr(dbData) && StringUtils.isNumeric(dbData)) { + return List.of(Long.parseLong(dbData)); + } + TypeReference<List<Long>> typeReference = new TypeReference<>() { + }; + List<Long> longList = JsonUtil.fromJson(dbData, typeReference); + if (longList == null && !dbData.isEmpty()) { + if (StringUtils.isNumeric(dbData)) { + return List.of(Long.parseLong(dbData)); + } else { + throw new NumberFormatException("String convert to Long error"); + } + } else { + return longList; + } + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonMapListAttributeConverter.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonMapListAttributeConverter.java new file mode 100644 index 0000000..62bc25a --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonMapListAttributeConverter.java @@ -0,0 +1,45 @@ +/* + * 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.manager; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.List; +import java.util.Map; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.stereotype.Component; +import tools.jackson.core.type.TypeReference; + +/** + * json map list str converter + */ +@Converter +@Component +public class JsonMapListAttributeConverter implements AttributeConverter<Map<String, List<String>>, String> { + + @Override + public String convertToDatabaseColumn(Map<String, List<String>> attribute) { + return JsonUtil.toJson(attribute); + } + + @Override + public Map<String, List<String>> convertToEntityAttribute(String dbData) { + TypeReference<Map<String, List<String>>> typeReference = new TypeReference<>() {}; + return JsonUtil.fromJson(dbData, typeReference); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonOptionListAttributeConverter.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonOptionListAttributeConverter.java new file mode 100644 index 0000000..de625e1 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonOptionListAttributeConverter.java @@ -0,0 +1,44 @@ +/* + * 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.manager; + +import tools.jackson.core.type.TypeReference; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.List; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.stereotype.Component; + +/** + * json str to list paramDefine.Option + */ +@Converter +@Component +public class JsonOptionListAttributeConverter implements AttributeConverter<List<ParamDefine.Option>, String> { + + @Override + public String convertToDatabaseColumn(List<ParamDefine.Option> attribute) { + return JsonUtil.toJson(attribute); + } + + @Override + public List<ParamDefine.Option> convertToEntityAttribute(String dbData) { + TypeReference<List<ParamDefine.Option>> typeReference = new TypeReference<>() {}; + return JsonUtil.fromJson(dbData, typeReference); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonStringListAttributeConverter.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonStringListAttributeConverter.java new file mode 100644 index 0000000..5866dd3 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/JsonStringListAttributeConverter.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.manager; + +import tools.jackson.core.type.TypeReference; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.stereotype.Component; + +/** + * Convert the list of strings to a JSON string + */ +@Converter +@Component +public class JsonStringListAttributeConverter implements AttributeConverter<List<String>, String> { + @Override + public String convertToDatabaseColumn(List<String> attribute) { + return JsonUtil.toJson(attribute); + + } + + @Override + public List<String> convertToEntityAttribute(String dbData) { + if (StringUtils.isBlank(dbData)) { + return List.of(); + } + if (!JsonUtil.isJsonStr(dbData)) { + return List.of(dbData); + } + TypeReference<List<String>> typeReference = new TypeReference<>() { + }; + List<String> stringList = JsonUtil.fromJson(dbData, typeReference); + if (stringList == null && !dbData.isEmpty()) { + return List.of(dbData); + } else { + return stringList; + } + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Label.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Label.java new file mode 100644 index 0000000..99b65e4 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Label.java @@ -0,0 +1,114 @@ +/* + * 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.manager; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import java.util.Objects; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Label Entity + */ +@Entity +@Table(name = "hzb_tag") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Label Entity") +@EntityListeners(AuditingEntityListener.class) +public class Label { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Primary key index ID", example = "87584674384", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "Label Field", example = "app", accessMode = READ_WRITE) + @NotBlank(message = "name can not null") + private String name; + + @Schema(title = "Label Value", example = "23", accessMode = READ_WRITE) + @Column(length = 2048) + private String tagValue; + + @Schema(title = "Label Description", example = "Used for monitoring mysql", accessMode = READ_WRITE) + private String description; + + @Schema(title = "Tag type 0: Auto-generated 1: user-generated 2: system preset", + accessMode = READ_WRITE) + @Min(0) + @Max(3) + private Byte type; + + @Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "The modifier of this record", example = "tom", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + @Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "1612198444000", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Label tag = (Label) o; + return Objects.equals(name, tag.name) && Objects.equals(tagValue, tag.tagValue); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 13 * hash + (name == null ? 0 : name.hashCode()) + (tagValue == null ? 0 : tagValue.hashCode()); + return hash; + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/MetricsFavorite.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/MetricsFavorite.java new file mode 100644 index 0000000..e1e1f99 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/MetricsFavorite.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.manager; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; + +/** + * Metrics Favorite Entity + */ +@Entity +@Table(name = "hzb_metrics_favorite", + uniqueConstraints = @UniqueConstraint(columnNames = {"creator", "monitor_id", "metrics_name"})) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MetricsFavorite { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "Creator cannot be null or blank") + @Size(max = 255, message = "Creator length cannot exceed 255 characters") + @Column(name = "creator", nullable = false) + private String creator; + + @NotNull(message = "Monitor ID cannot be null") + @Column(name = "monitor_id", nullable = false) + private Long monitorId; + + @NotBlank(message = "Metrics name cannot be null or blank") + @Size(max = 255, message = "Metrics name length cannot exceed 255 characters") + @Column(name = "metrics_name", nullable = false) + private String metricsName; + + @CreatedDate + @Column(name = "create_time", updatable = false) + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java new file mode 100644 index 0000000..95ff7a0 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java @@ -0,0 +1,144 @@ +/* + * 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.manager; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.alerter.JsonMapAttributeConverter; +import org.apache.hertzbeat.common.support.valid.HostValid; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Monitor Entity + */ +@Entity +@Table(name = "hzb_monitor", indexes = { + @Index(name = "idx_hzb_monitor_app", columnList = "app"), + @Index(name = "idx_hzb_monitor_instance", columnList = "instance"), + @Index(name = "idx_hzb_monitor_name", columnList = "name") +}) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Monitor Entity") +@EntityListeners(AuditingEntityListener.class) +public class Monitor { + + @Id + @Schema(title = "Monitor task ID", example = "87584674384", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "Collect task ID", example = "43243543543", accessMode = READ_ONLY) + private Long jobId; + + @Schema(title = "task name", example = "Api-TanCloud.cn", accessMode = READ_WRITE) + @Size(max = 100) + private String name; + + @Schema(title = "Type of monitoring", example = "TanCloud", accessMode = READ_WRITE) + @Size(max = 100) + private String app; + + @Schema(title = "Scrape type: static | http_sd | dns_sd | zookeeper_sd", example = "static", accessMode = READ_WRITE) + @Size(max = 100) + private String scrape; + + @Schema(title = "the monitor target: ip/domain+port or ip/domain", example = "192.167.25.11:8081", accessMode = READ_WRITE) + @Size(max = 100) + @HostValid + private String instance; + + @Schema(title = "Monitoring of the acquisition interval time in seconds", example = "600", accessMode = READ_WRITE) + @Min(10) + private Integer intervals; + + @Schema(title = "Schedule type: interval | cron", example = "interval", accessMode = READ_WRITE) + @Size(max = 20) + private String scheduleType; + + @Schema(title = "Cron expression when scheduleType is cron", example = "0/5 * * * * ?", accessMode = READ_WRITE) + @Size(max = 100) + private String cronExpression; + + @Schema(title = "Task status 0: Paused, 1: Up, 2: Down", accessMode = READ_WRITE) + @Min(0) + @Max(4) + private byte status; + + @Schema(title = "Task type 0: Normal, 1: push auto create, 2: discovery auto create") + private byte type; + + @Schema(title = "task label", example = "{env:test}", accessMode = READ_WRITE) + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 4096) + private Map<String, String> labels; + + @Schema(title = "task annotations", example = "{summary:this task looks good}", accessMode = READ_WRITE) + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 4096) + private Map<String, String> annotations; + + @Schema(title = "Monitor note description", example = "Availability monitoring of the SAAS website TanCloud", accessMode = READ_WRITE) + @Size(max = 255) + private String description; + + @Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "The modifier of this record", example = "tom", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + @Schema(title = "Record create time", example = "2024-07-02T20:09:34.903217", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "2024-07-02T20:09:34.903217", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; + + @Override + public Monitor clone() { + return JsonUtil.fromJson(JsonUtil.toJson(this), getClass()); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/MonitorBind.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/MonitorBind.java new file mode 100644 index 0000000..885c99b --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/MonitorBind.java @@ -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.common.entity.manager; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Monitor Bind + */ +@Entity +@Table(name = "hzb_monitor_bind", indexes = { + @Index(name = "index_monitor_bind", columnList = "biz_id"), + @Index(name = "index_monitor_bin", columnList = "monitor_id") +}) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "relation between monitor") +@EntityListeners(AuditingEntityListener.class) +public class MonitorBind { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "primary id", example = "23") + private Long id; + + @Schema(title = "key string: ip:port") + private String keyStr; + + @Schema(title = "connect bind id", example = "87432674384") + private Long bizId; + + @Schema(title = "monitor ID", example = "87432674336") + @Column(name = "monitor_id") + private Long monitorId; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by") + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)") + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Param.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Param.java new file mode 100644 index 0000000..7829ca8 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/Param.java @@ -0,0 +1,121 @@ +/* + * 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.manager; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Monitor parameter values + */ +@Entity +@Table( + name = "hzb_param", + indexes = {@Index(name = "idx_hzb_param_monitor_id", columnList = "monitor_id")}, + uniqueConstraints = {@UniqueConstraint(name = "uk_hzb_param_monitor_field", columnNames = {"monitor_id", "field"})} +) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Parameter Entity") +@EntityListeners(AuditingEntityListener.class) +public class Param { + + /** + * Parameter primary key index ID + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Parameter primary key index ID", example = "87584674384", accessMode = READ_ONLY) + private Long id; + + /** + * Monitor ID + */ + @Schema(title = "Monitor task ID", example = "875846754543", accessMode = READ_WRITE) + @Column(name = "monitor_id") + private Long monitorId; + + /** + * Parameter Field Identifier + */ + @Schema(title = "Parameter identifier field", example = "port", accessMode = READ_WRITE) + @Size(max = 100) + @Column(name = "field") + @NotBlank(message = "field can not null") + private String field; + + /** + * Param Value + */ + @Schema(title = "parameter values", example = "8080", accessMode = READ_WRITE) + @Size(max = 8126) + @Column(length = 8126) + private String paramValue; + + /** + * Parameter type 0: number 1: string 2: encrypted string 3: json string mapped by map + */ + @Schema(title = "Parameter types 0: number 1: string 2: encrypted string 3:map mapped json string 4:arrays string", + accessMode = READ_WRITE) + @Min(0) + private byte type; + + /** + * Record create time + */ + @Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + /** + * Record the latest modification time + */ + @Schema(title = "Record modify time", example = "1612198444000", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; + + @Override + public Param clone() { + // deep clone + return JsonUtil.fromJson(JsonUtil.toJson(this), getClass()); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java new file mode 100644 index 0000000..d4b54d2 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java @@ -0,0 +1,212 @@ +/* + * 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.manager; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.alerter.JsonMapAttributeConverter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Monitoring parameter definitions + */ +@Entity +@Table(name = "hzb_param_define") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Parameter structure definition entity") +@EntityListeners(AuditingEntityListener.class) +public class ParamDefine { + + /** + * Parameter Structure ID + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Parameter structure ID", example = "87584674384", accessMode = READ_ONLY) + private Long id; + + /** + * Monitoring application type name + */ + @Schema(title = "Type of monitoring", example = "TanCloud", accessMode = READ_WRITE) + private String app; + + /** + * Parameter field external display name + * Port + */ + @Schema(description = "The parameter field displays the internationalized name", example = "{zh-CN: '端口'}", + accessMode = READ_WRITE) + @Convert(converter = JsonMapAttributeConverter.class) + @SuppressWarnings("JpaAttributeTypeInspection") + @Column(length = 2048) + private Map<String, String> name; + + /** + * Parameter Field Identifier + */ + @Schema(title = "Parameter field identifier", example = "port", accessMode = READ_WRITE) + private String field; + + /** + * Field type, style (mostly map the input tag type attribute) + */ + @Schema(title = "Field type, style (mostly map the input tag type attribute)", example = "number", accessMode = READ_WRITE) + private String type; + + /** + * Is it mandatory true-required false-optional + */ + @Schema(title = "Is it mandatory true-required false-optional", example = "true", accessMode = READ_WRITE) + private boolean required = false; + + /** + * Parameter Default Value + */ + @Schema(title = "Parameter default values", example = "12", accessMode = READ_WRITE) + private String defaultValue; + + /** + * Parameter input box prompt information + */ + @Schema(title = "Parameter input field prompt information", example = "enter your password", accessMode = READ_WRITE) + private String placeholder; + + /** + * When type is number, use range to represent the range eg: 0-233 + */ + @Schema(title = "When type is number, the range is represented by the range interval", example = "[0,233]", accessMode = READ_WRITE) + @Column(name = "param_range") + private String range; + + /** + * When type is text, use limit to indicate the limit size of the string. The maximum is 255 + */ + @Schema(title = "When type is text, use limit to indicate the limit size of the string. The maximum is 255", + example = "30", accessMode = READ_WRITE) + @Column(name = "param_limit") + private Short limit; + + /** + * When the type is radio radio box, checkbox checkbox, options represents a list of optional values + * eg: { + * "key1":"value1", + * "key2":"value2" + * } + * key-Value display label + * value-True value + */ + @Schema(description = "When the type is radio radio box, checkbox checkbox, options represents a list of optional values", + example = "{key1,value1}", accessMode = READ_WRITE) + @Column(name = "param_options", length = 2048) + @Convert(converter = JsonOptionListAttributeConverter.class) + private List<Option> options; + + /** + * Valid when type is key-value, indicating the alias description of the key + */ + @Schema(title = "Valid when type is key-value, indicating the alias description of the key", example = "Name", accessMode = READ_WRITE) + private String keyAlias; + + /** + * Valid when type is key-value, indicating the alias description of value type + */ + @Schema(title = "Valid when type is key-value, indicating the alias description of value type", example = "Value", accessMode = READ_WRITE) + private String valueAlias; + + /** + * Is it an advanced hidden parameter true-yes false-no + */ + @Schema(title = "Is it an advanced hidden parameter true-yes false-no", example = "true", accessMode = READ_WRITE) + private boolean hide = false; + + /** + * The creator of this record + */ + @Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + /** + * This record was last modified by + */ + @Schema(title = "The modifier of this record", example = "tom", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + /** + * Record create time + */ + @Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + /** + * Record the latest modification time + */ + @Schema(title = "Record modify time", example = "1612198444000", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; + + /** + * Depends on which parameters + */ + @Schema(title = "Depends on which parameters", example = "{field:[value1, value2, ...]}", accessMode = READ_WRITE) + @Convert(converter = JsonMapAttributeConverter.class) + private Map<String, List<Object>> depend; + + /** + * Parameter option configuration + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static final class Option { + /** + * value display label + */ + private String label; + /** + * optional value + */ + private String value; + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/PluginItem.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/PluginItem.java new file mode 100644 index 0000000..a0db48e --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/PluginItem.java @@ -0,0 +1,94 @@ +/* + * 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.manager; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.util.Objects; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.constants.PluginType; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Plugin Entity + */ +@Entity +@Table(name = "hzb_plugin_item") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "PluginItem Entity") +@EntityListeners(AuditingEntityListener.class) +public class PluginItem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Plugin Primary key index ID", example = "87584674384", accessMode = READ_ONLY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "metadata_id") + @JsonIgnore + private PluginMetadata pluginMetadata; + + @Schema(title = "Plugin implementation class full path", example = "org.apache.hertzbeat.plugin.impl.DemoPluginImpl", accessMode = READ_WRITE) + private String classIdentifier; + + @Schema(title = "Plugin type", example = "POST_ALERT", accessMode = READ_WRITE) + @Enumerated(EnumType.STRING) + private PluginType type; + + public PluginItem(String classIdentifier, PluginType type) { + this.classIdentifier = classIdentifier; + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PluginItem that = (PluginItem) o; + return Objects.equals(id, that.id) && Objects.equals(classIdentifier, that.classIdentifier) && type == that.type; + } + + @Override + public int hashCode() { + return Objects.hash(id, classIdentifier, type); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/PluginMetadata.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/PluginMetadata.java new file mode 100644 index 0000000..55b12b3 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/PluginMetadata.java @@ -0,0 +1,107 @@ +/* + * 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.manager; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Plugin Entity + */ +@Entity +@Table(name = "hzb_plugin_metadata") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Plugin Entity") +@EntityListeners(AuditingEntityListener.class) +public class PluginMetadata { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Plugin Primary key index ID", example = "87584674384", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "plugin name", example = "notification plugin", accessMode = READ_WRITE) + @NotNull + private String name; + + @Schema(title = "Plugin activation status", example = "true", accessMode = READ_WRITE) + private Boolean enableStatus; + + @Schema(title = "Jar file path", example = "true", accessMode = READ_WRITE) + private String jarFilePath; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PluginMetadata that = (PluginMetadata) o; + return Objects.equals(id, that.id) && Objects.equals(name, that.name) && Objects.equals(enableStatus, that.enableStatus) && Objects.equals(jarFilePath, + that.jarFilePath) && Objects.equals(creator, that.creator) && Objects.equals(gmtCreate, that.gmtCreate); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, enableStatus, jarFilePath, creator, gmtCreate); + } + + @Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @OneToMany(targetEntity = PluginItem.class, cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinColumn(name = "metadata_id", referencedColumnName = "id") + private List<PluginItem> items; + + @Schema(title = "Param count", example = "1", accessMode = READ_WRITE) + private Integer paramCount; + + +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageComponent.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageComponent.java new file mode 100644 index 0000000..0bf6d57 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageComponent.java @@ -0,0 +1,101 @@ +/* + * 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.manager; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.alerter.JsonMapAttributeConverter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * status page component entity + */ +@Entity +@Table(name = "hzb_status_page_component") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "status page component entity") +@EntityListeners(AuditingEntityListener.class) +public class StatusPageComponent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "ID", example = "87584674384") + private Long id; + + @Schema(title = "org id", example = "1234") + private Long orgId; + + @Schema(title = "component name", example = "Gateway") + @NotBlank + private String name; + + @Schema(title = "component desc", example = "TanCloud Gateway") + private String description; + + @Schema(title = "component label", example = "{env:test}", accessMode = READ_WRITE) + @Convert(converter = JsonMapAttributeConverter.class) + @Column(length = 4096) + private Map<String, String> labels; + + @Schema(title = "calculate status method: 0-auto 1-manual", example = "0") + private byte method; + + @Schema(title = "config state when use manual method: 0-Normal 1-Abnormal 2-unknown", example = "0") + private byte configState; + + @Schema(title = "component current state: 0-Normal 1-Abnormal 2-unknown", example = "0") + private byte state; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "The modifier of this record", example = "tom") + @LastModifiedBy + private String modifier; + + @Schema(title = "Record create time", example = "1612198922000") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "1612198444000") + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageHistory.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageHistory.java new file mode 100644 index 0000000..b384956 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageHistory.java @@ -0,0 +1,91 @@ +/* + * 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.manager; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * status page history entity + */ +@Entity +@Table(name = "hzb_status_page_history") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "status page component history entity") +@EntityListeners(AuditingEntityListener.class) +public class StatusPageHistory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "ID", example = "87584674384") + private Long id; + + @Schema(title = "component id", example = "1234") + private Long componentId; + + @Schema(title = "component state: 0-Normal 1-Abnormal 2-unknown", example = "0") + private byte state; + + @Schema(title = "state calculate timestamp", example = "4248574985744") + private Long timestamp; + + @Schema(title = "state uptime percentage", example = "99.99") + private Double uptime; + + @Schema(title = "state abnormal time(s)", example = "1000") + private Integer abnormal; + + @Schema(title = "state unknown time(s)", example = "1000") + private Integer unknowing; + + @Schema(title = "state normal tim(s)", example = "1000") + private Integer normal; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "The modifier of this record", example = "tom") + @LastModifiedBy + private String modifier; + + @Schema(title = "Record create time", example = "1612198922000") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "1612198444000") + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageIncident.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageIncident.java new file mode 100644 index 0000000..2df6d6d --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageIncident.java @@ -0,0 +1,112 @@ +/* + * 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.manager; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.CascadeType; +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * status page incident entity + */ +@Entity +@Table(name = "hzb_status_page_incident") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "status page incident entity") +@EntityListeners(AuditingEntityListener.class) +public class StatusPageIncident { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "ID", example = "87584674384") + private Long id; + + @Schema(title = "org id", example = "1234") + private Long orgId; + + @Schema(title = "incident name", example = "Gateway") + @NotBlank + private String name; + + @Schema(title = "incident current state: 0-Investigating 1-Identified 2-Monitoring 3-Resolved", example = "0") + private byte state; + + @Schema(title = "incident start Investigating timestamp", example = "4248574985744") + private Long startTime; + + @Schema(title = "incident end Resolved timestamp", example = "4248574985744") + private Long endTime; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "The modifier of this record", example = "tom") + @LastModifiedBy + private String modifier; + + @Schema(title = "Record create time", example = "1612198922000") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "1612198444000") + @LastModifiedDate + private LocalDateTime gmtUpdate; + + @Schema(title = "status page components") + @ManyToMany(targetEntity = StatusPageComponent.class, cascade = CascadeType.MERGE, fetch = FetchType.EAGER) + @JoinTable(name = "hzb_status_page_incident_component_bind", + foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), + inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), + joinColumns = {@JoinColumn(name = "incident_id", referencedColumnName = "id")}, + inverseJoinColumns = {@JoinColumn(name = "component_id", referencedColumnName = "id")}) + private List<StatusPageComponent> components; + + @Schema(title = "status page incident content") + @OneToMany(targetEntity = StatusPageIncidentContent.class, cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinColumn(name = "incident_id", referencedColumnName = "id") + private Set<StatusPageIncidentContent> contents; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageIncidentComponentBind.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageIncidentComponentBind.java new file mode 100644 index 0000000..a7941df --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageIncidentComponentBind.java @@ -0,0 +1,74 @@ +/* + * 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.manager; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * status page incident component bind entity + */ +@Entity +@Table(name = "hzb_status_page_incident_component_bind", indexes = { + @Index(name = "index_incident_component", columnList = "incident_id"), +}) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Incident Component Bind") +@EntityListeners(AuditingEntityListener.class) +public class StatusPageIncidentComponentBind { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Identity", example = "87584674384") + private Long id; + + @Schema(title = "Incident ID", example = "87432674384") + @Column(name = "incident_id") + private Long incidentId; + + @Schema(title = "Component ID", example = "87432674336") + @Column(name = "component_id") + private Long componentId; + + @Schema(title = "Record create time", example = "1612198922000") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "1612198444000") + @LastModifiedDate + private LocalDateTime gmtUpdate; + +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageIncidentContent.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageIncidentContent.java new file mode 100644 index 0000000..0d74dd6 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageIncidentContent.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.manager; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * status page incident entity content. + */ +@Entity +@Table(name = "hzb_status_page_incident_content") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "status page incident content entity") +@EntityListeners(AuditingEntityListener.class) +public class StatusPageIncidentContent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "ID", example = "87584674384") + private Long id; + + @Schema(title = "org id", example = "1234") + @Column(name = "incident_id") + private Long incidentId; + + @Schema(title = "incident content message", example = "we find the gateway connection timeout") + @NotBlank + private String message; + + @Schema(title = "incident state: 0-Investigating 1-Identified 2-Monitoring 3-Resolved", example = "0") + private byte state; + + @Schema(title = "incident content message timestamp", example = "4248574985744") + private Long timestamp; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "The modifier of this record", example = "tom") + @LastModifiedBy + private String modifier; + + @Schema(title = "Record create time", example = "1612198922000") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "1612198444000") + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageOrg.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageOrg.java new file mode 100644 index 0000000..567feb2 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/StatusPageOrg.java @@ -0,0 +1,98 @@ +/* + * 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.manager; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * status page org entity + */ +@Entity +@Table(name = "hzb_status_page_org") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "status page org entity") +@EntityListeners(AuditingEntityListener.class) +public class StatusPageOrg { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "ID", example = "87584674384") + private Long id; + + @Schema(title = "org name", example = "TanCloud") + @NotBlank + private String name; + + @Schema(title = "org desc", example = "TanCloud inc") + @NotBlank + private String description; + + @Schema(title = "org home url", example = "https://tancloud.com") + @NotBlank + private String home; + + @Schema(title = "org logo url", example = "logo.svg url") + @NotBlank + private String logo; + + @Schema(title = "org feedback issue url", example = "contact@email.com") + private String feedback; + + @Schema(title = "org theme background color", example = "#ffffff") + private String color; + + @Schema(title = "org current state: 0-All Systems Operational 1-Some Systems Abnormal 2-All Systems Abnormal ", + example = "0") + private byte state; + + @Schema(title = "The creator of this record", example = "tom") + @CreatedBy + private String creator; + + @Schema(title = "The modifier of this record", example = "tom") + @LastModifiedBy + private String modifier; + + @Schema(title = "Record create time", example = "1612198922000") + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record modify time", example = "1612198444000") + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ZonedDateTimeAttributeConverter.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ZonedDateTimeAttributeConverter.java new file mode 100644 index 0000000..bbbaf00 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ZonedDateTimeAttributeConverter.java @@ -0,0 +1,48 @@ +/* + * 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.manager; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import org.springframework.stereotype.Component; + +/** + * zone data time to OffsetDateTime + */ +@Converter +@Component +public class ZonedDateTimeAttributeConverter implements AttributeConverter<ZonedDateTime, OffsetDateTime> { + + @Override + public OffsetDateTime convertToDatabaseColumn(ZonedDateTime attribute) { + if (attribute == null) { + return null; + } + return attribute.toOffsetDateTime(); + } + + @Override + public ZonedDateTime convertToEntityAttribute(OffsetDateTime dbData) { + if (dbData == null) { + return null; + } + return dbData.toZonedDateTime(); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetrics.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetrics.java new file mode 100644 index 0000000..5c58cfb --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/push/PushMetrics.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.push; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * push metrics entity + */ +@Entity +@Table(name = "hzb_push_metrics", indexes = { + @Index(name = "idx_push_metrics_monitor_id", columnList = "monitor_id"), + @Index(name = "idx_push_metrics_time", columnList = "time") +}) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class PushMetrics { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private Long monitorId; + private Long time; + private String metrics; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/warehouse/History.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/warehouse/History.java new file mode 100644 index 0000000..6e29d33 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/warehouse/History.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.entity.warehouse; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * metrics history data entity + */ +@Entity +@Table(name = "hzb_history", indexes = { + @Index(name = "idx_hzb_history_instance", columnList = "instance"), + @Index(name = "idx_hzb_history_app", columnList = "app"), + @Index(name = "idx_hzb_history_metrics", columnList = "metrics"), + @Index(name = "idx_hzb_history_metric", columnList = "metric") +}) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Metrics History Data Entity") +public class History { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Metric data history entity primary key index ID", example = "87584674384", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "Monitoring instance", example = "127.0.0.1:8080", accessMode = READ_WRITE) + private String instance; + + @Schema(title = "Monitoring Type mysql oracle db2") + private String app; + + @Schema(title = "Monitoring Metrics innodb disk cpu") + private String metrics; + + @Schema(title = "Monitoring Metric usage speed count") + private String metric; + + @Column(length = 5000) + private String metricLabels; + + @Schema(title = "Metric Type 0: Number 1:String") + private Byte metricType; + + @Schema(title = "Metric String Value") + @Column(length = 2048) + private String str; + + @Schema(title = "Metric Integer Value") + private Integer int32; + + @Schema(title = "Metric Number Value") + private Double dou; + + @Schema(title = "Collect Time") + private Long time; + +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/queue/impl/InMemoryCommonDataQueue.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/queue/impl/InMemoryCommonDataQueue.java new file mode 100644 index 0000000..9a9e226 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/queue/impl/InMemoryCommonDataQueue.java @@ -0,0 +1,173 @@ +/* + * 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.queue.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.DataQueueConstants; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * common data queue implement memory + */ +@Configuration +@ConditionalOnProperty( + prefix = DataQueueConstants.PREFIX, + name = DataQueueConstants.NAME, + havingValue = DataQueueConstants.IN_MEMORY, + matchIfMissing = true +) +@Slf4j +@Primary +public class InMemoryCommonDataQueue implements CommonDataQueue, DisposableBean { + + private final LinkedBlockingQueue<CollectRep.MetricsData> metricsDataToAlertQueue; + private final LinkedBlockingQueue<CollectRep.MetricsData> metricsDataToStorageQueue; + private final LinkedBlockingQueue<CollectRep.MetricsData> serviceDiscoveryDataQueue; + private final LinkedBlockingQueue<LogEntry> logEntryQueue; + private final LinkedBlockingQueue<LogEntry> logEntryToStorageQueue; + + public InMemoryCommonDataQueue() { + metricsDataToAlertQueue = new LinkedBlockingQueue<>(); + metricsDataToStorageQueue = new LinkedBlockingQueue<>(); + serviceDiscoveryDataQueue = new LinkedBlockingQueue<>(); + logEntryQueue = new LinkedBlockingQueue<>(); + logEntryToStorageQueue = new LinkedBlockingQueue<>(); + } + + public Map<String, Integer> getQueueSizeMetricsInfo() { + Map<String, Integer> metrics = new HashMap<>(8); + metrics.put("metricsDataToAlertQueue", metricsDataToAlertQueue.size()); + metrics.put("metricsDataToStorageQueue", metricsDataToStorageQueue.size()); + metrics.put("logEntryQueue", logEntryQueue.size()); + metrics.put("logEntryToStorageQueue", logEntryToStorageQueue.size()); + return metrics; + } + + @Override + public CollectRep.MetricsData pollServiceDiscoveryData() throws InterruptedException { + return serviceDiscoveryDataQueue.take(); + } + + @Override + public CollectRep.MetricsData pollMetricsDataToAlerter() throws InterruptedException { + return metricsDataToAlertQueue.take(); + } + + @Override + public CollectRep.MetricsData pollMetricsDataToStorage() throws InterruptedException { + return metricsDataToStorageQueue.take(); + } + + @Override + public void sendMetricsData(CollectRep.MetricsData metricsData) { + metricsDataToAlertQueue.offer(metricsData); + } + + @Override + public void sendMetricsDataToStorage(CollectRep.MetricsData metricsData) { + metricsDataToStorageQueue.offer(metricsData); + } + + @Override + public void sendServiceDiscoveryData(CollectRep.MetricsData metricsData) { + serviceDiscoveryDataQueue.offer(metricsData); + } + + @Override + public void sendLogEntry(LogEntry logEntry) { + logEntryQueue.offer(logEntry); + } + + @Override + public LogEntry pollLogEntry() throws InterruptedException { + return logEntryQueue.take(); + } + + @Override + public void sendLogEntryToStorage(LogEntry logEntry) { + logEntryToStorageQueue.offer(logEntry); + } + + @Override + public LogEntry pollLogEntryToStorage() throws InterruptedException { + return logEntryToStorageQueue.take(); + } + + @Override + public void sendLogEntryToAlertBatch(List<LogEntry> logEntries) { + if (logEntries == null || logEntries.isEmpty()) { + return; + } + for (LogEntry logEntry : logEntries) { + logEntryQueue.offer(logEntry); + } + } + + @Override + public List<LogEntry> pollLogEntryToAlertBatch(int maxBatchSize) throws InterruptedException { + List<LogEntry> batch = new ArrayList<>(maxBatchSize); + LogEntry first = logEntryQueue.poll(1, TimeUnit.SECONDS); + if (first != null) { + batch.add(first); + logEntryQueue.drainTo(batch, maxBatchSize - 1); + } + return batch; + } + + @Override + public void sendLogEntryToStorageBatch(List<LogEntry> logEntries) { + if (logEntries == null || logEntries.isEmpty()) { + return; + } + for (LogEntry logEntry : logEntries) { + logEntryToStorageQueue.offer(logEntry); + } + } + + @Override + public List<LogEntry> pollLogEntryToStorageBatch(int maxBatchSize) throws InterruptedException { + List<LogEntry> batch = new ArrayList<>(maxBatchSize); + LogEntry first = logEntryToStorageQueue.poll(1, TimeUnit.SECONDS); + if (first != null) { + batch.add(first); + logEntryToStorageQueue.drainTo(batch, maxBatchSize - 1); + } + return batch; + } + + @Override + public void destroy() { + metricsDataToAlertQueue.clear(); + metricsDataToStorageQueue.clear(); + serviceDiscoveryDataQueue.clear(); + logEntryQueue.clear(); + logEntryToStorageQueue.clear(); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/queue/impl/KafkaCommonDataQueue.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/queue/impl/KafkaCommonDataQueue.java new file mode 100644 index 0000000..4e77a7f --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/queue/impl/KafkaCommonDataQueue.java @@ -0,0 +1,376 @@ +/* + * 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.queue.impl; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.locks.ReentrantLock; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.config.CommonProperties; +import org.apache.hertzbeat.common.constants.DataQueueConstants; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.common.serialize.KafkaLogEntryDeserializer; +import org.apache.hertzbeat.common.serialize.KafkaLogEntrySerializer; +import org.apache.hertzbeat.common.serialize.KafkaMetricsDataDeserializer; +import org.apache.hertzbeat.common.serialize.KafkaMetricsDataSerializer; +import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.LongDeserializer; +import org.apache.kafka.common.serialization.LongSerializer; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; + + +/** + * common data queue implement kafka + */ +@Configuration +@ConditionalOnProperty( + prefix = DataQueueConstants.PREFIX, + name = DataQueueConstants.NAME, + havingValue = DataQueueConstants.KAFKA +) +@Slf4j +public class KafkaCommonDataQueue implements CommonDataQueue, DisposableBean { + + private final ReentrantLock metricDataToAlertLock = new ReentrantLock(); + private final ReentrantLock metricDataToStorageLock = new ReentrantLock(); + private final ReentrantLock serviceDiscoveryDataLock = new ReentrantLock(); + private final ReentrantLock logEntryLock = new ReentrantLock(); + private final ReentrantLock logEntryToStorageLock = new ReentrantLock(); + private final LinkedBlockingQueue<CollectRep.MetricsData> metricsDataToAlertQueue; + private final LinkedBlockingQueue<CollectRep.MetricsData> metricsDataToStorageQueue; + private final LinkedBlockingQueue<CollectRep.MetricsData> serviceDiscoveryDataQueue; + private final LinkedBlockingQueue<LogEntry> logEntryQueue; + private final LinkedBlockingQueue<LogEntry> logEntryToStorageQueue; + private final CommonProperties.KafkaProperties kafka; + private KafkaProducer<Long, CollectRep.MetricsData> metricsDataProducer; + private KafkaProducer<Long, LogEntry> logEntryProducer; + private KafkaConsumer<Long, CollectRep.MetricsData> metricsDataToAlertConsumer; + private KafkaConsumer<Long, CollectRep.MetricsData> metricsDataToStorageConsumer; + private KafkaConsumer<Long, CollectRep.MetricsData> serviceDiscoveryDataConsumer; + private KafkaConsumer<Long, LogEntry> logEntryConsumer; + private KafkaConsumer<Long, LogEntry> logEntryToStorageConsumer; + + public KafkaCommonDataQueue(CommonProperties properties) { + if (properties == null || properties.getQueue() == null || properties.getQueue().getKafka() == null) { + log.error("init error, please config common.queue.kafka props in application.yml"); + throw new IllegalArgumentException("please config common.queue.kafka props"); + } + this.kafka = properties.getQueue().getKafka(); + metricsDataToAlertQueue = new LinkedBlockingQueue<>(); + metricsDataToStorageQueue = new LinkedBlockingQueue<>(); + serviceDiscoveryDataQueue = new LinkedBlockingQueue<>(); + logEntryQueue = new LinkedBlockingQueue<>(); + logEntryToStorageQueue = new LinkedBlockingQueue<>(); + initDataQueue(); + } + + private void initDataQueue() { + try { + Map<String, Object> producerConfig = new HashMap<>(3); + producerConfig.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getServers()); + producerConfig.put(ProducerConfig.ACKS_CONFIG, "all"); + producerConfig.put(ProducerConfig.RETRIES_CONFIG, 3); + metricsDataProducer = new KafkaProducer<>(producerConfig, new LongSerializer(), new KafkaMetricsDataSerializer()); + logEntryProducer = new KafkaProducer<>(producerConfig, new LongSerializer(), new KafkaLogEntrySerializer()); + + Map<String, Object> consumerConfig = new HashMap<>(4); + consumerConfig.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getServers()); + consumerConfig.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "50"); + consumerConfig.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); + // 15 minute + consumerConfig.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "900000"); + consumerConfig.put("group.id", "default-consumer"); + + Map<String, Object> metricsToAlertConsumerConfig = new HashMap<>(consumerConfig); + metricsToAlertConsumerConfig.put("group.id", "metrics-alert-consumer"); + metricsDataToAlertConsumer = new KafkaConsumer<>(metricsToAlertConsumerConfig, new LongDeserializer(), new KafkaMetricsDataDeserializer()); + metricsDataToAlertConsumer.subscribe(Collections.singletonList(kafka.getMetricsDataTopic())); + + Map<String, Object> metricsToStorageConsumerConfig = new HashMap<>(consumerConfig); + metricsToStorageConsumerConfig.put("group.id", "metrics-persistent-consumer"); + metricsDataToStorageConsumer = new KafkaConsumer<>(metricsToStorageConsumerConfig, new LongDeserializer(), new KafkaMetricsDataDeserializer()); + metricsDataToStorageConsumer.subscribe(Collections.singletonList(kafka.getMetricsDataToStorageTopic())); + + Map<String, Object> serviceDiscoveryDataConsumerConfig = new HashMap<>(consumerConfig); + serviceDiscoveryDataConsumerConfig.put("group.id", "service-discovery-data-consumer"); + serviceDiscoveryDataConsumer = new KafkaConsumer<>(serviceDiscoveryDataConsumerConfig, new LongDeserializer(), + new KafkaMetricsDataDeserializer()); + serviceDiscoveryDataConsumer.subscribe(Collections.singletonList(kafka.getServiceDiscoveryDataTopic())); + + Map<String, Object> logEntryConsumerConfig = new HashMap<>(consumerConfig); + logEntryConsumerConfig.put("group.id", "log-entry-consumer"); + logEntryConsumer = new KafkaConsumer<>(logEntryConsumerConfig, new LongDeserializer(), new KafkaLogEntryDeserializer()); + logEntryConsumer.subscribe(Collections.singletonList(kafka.getLogEntryDataTopic())); + + Map<String, Object> logEntryToStorageConsumerConfig = new HashMap<>(consumerConfig); + logEntryToStorageConsumerConfig.put("group.id", "log-entry-storage-consumer"); + logEntryToStorageConsumer = new KafkaConsumer<>(logEntryToStorageConsumerConfig, new LongDeserializer(), new KafkaLogEntryDeserializer()); + logEntryToStorageConsumer.subscribe(Collections.singletonList(kafka.getLogEntryDataToStorageTopic())); + } catch (Exception e) { + log.error("please config common.queue.kafka props correctly", e); + throw e; + } + } + + @Override + public CollectRep.MetricsData pollServiceDiscoveryData() throws InterruptedException { + return genericPollDataFunction(serviceDiscoveryDataQueue, serviceDiscoveryDataConsumer, serviceDiscoveryDataLock); + } + + @Override + public CollectRep.MetricsData pollMetricsDataToAlerter() throws InterruptedException { + return genericPollDataFunction(metricsDataToAlertQueue, metricsDataToAlertConsumer, metricDataToAlertLock); + } + + @Override + public CollectRep.MetricsData pollMetricsDataToStorage() throws InterruptedException { + return genericPollDataFunction(metricsDataToStorageQueue, metricsDataToStorageConsumer, metricDataToStorageLock); + } + + public <T> T genericPollDataFunction(LinkedBlockingQueue<T> dataQueue, KafkaConsumer<Long, T> dataConsumer, ReentrantLock lock) throws InterruptedException { + + T pollData = dataQueue.poll(); + if (pollData != null) { + return pollData; + } + lock.lockInterruptibly(); + try { + ConsumerRecords<Long, T> records = dataConsumer.poll(Duration.ofSeconds(1)); + int index = 0; + for (ConsumerRecord<Long, T> record : records) { + if (index == 0) { + pollData = record.value(); + } else { + dataQueue.offer(record.value()); + } + index++; + } + dataConsumer.commitAsync(); + } catch (Exception e) { + log.error(e.getMessage()); + throw new CommonDataQueueUnknownException(e.getMessage(), e); + } finally { + lock.unlock(); + } + return pollData; + } + + @Override + public void sendMetricsData(CollectRep.MetricsData metricsData) { + if (metricsDataProducer != null) { + ProducerRecord<Long, CollectRep.MetricsData> record = + new ProducerRecord<>(kafka.getMetricsDataTopic(), metricsData); + metricsDataProducer.send(record); + } else { + log.error("metricsDataProducer is not enabled"); + } + } + + @Override + public void sendMetricsDataToStorage(CollectRep.MetricsData metricsData) { + if (metricsDataProducer != null) { + ProducerRecord<Long, CollectRep.MetricsData> record = + new ProducerRecord<>(kafka.getMetricsDataToStorageTopic(), metricsData); + metricsDataProducer.send(record); + } else { + log.error("metricsDataProducer is not enabled"); + } + } + + @Override + public void sendServiceDiscoveryData(CollectRep.MetricsData metricsData) { + if (metricsDataProducer != null) { + ProducerRecord<Long, CollectRep.MetricsData> record = + new ProducerRecord<>(kafka.getServiceDiscoveryDataTopic(), metricsData); + metricsDataProducer.send(record); + } else { + log.error("metricsDataProducer is not enabled"); + } + } + + @Override + public void sendLogEntry(LogEntry logEntry) { + if (logEntryProducer != null) { + try { + ProducerRecord<Long, LogEntry> record = new ProducerRecord<>(kafka.getLogEntryDataTopic(), logEntry); + logEntryProducer.send(record); + } catch (Exception e) { + log.error("Failed to send LogEntry to Kafka: {}", e.getMessage()); + // Fallback to memory queue if Kafka fails + logEntryQueue.offer(logEntry); + } + } else { + log.warn("logEntryProducer is not enabled, using memory queue"); + logEntryQueue.offer(logEntry); + } + } + + @Override + public LogEntry pollLogEntry() throws InterruptedException { + return genericPollDataFunction(logEntryQueue, logEntryConsumer, logEntryLock); + } + + @Override + public void sendLogEntryToStorage(LogEntry logEntry) { + if (logEntryProducer != null) { + try { + ProducerRecord<Long, LogEntry> record = new ProducerRecord<>(kafka.getLogEntryDataToStorageTopic(), logEntry); + logEntryProducer.send(record); + } catch (Exception e) { + log.error("Failed to send LogEntry to storage via Kafka: {}", e.getMessage()); + // Fallback to memory queue if Kafka fails + logEntryToStorageQueue.offer(logEntry); + } + } else { + log.warn("logEntryProducer is not enabled, using memory queue for storage"); + logEntryToStorageQueue.offer(logEntry); + } + } + + @Override + public LogEntry pollLogEntryToStorage() throws InterruptedException { + return genericPollDataFunction(logEntryToStorageQueue, logEntryToStorageConsumer, logEntryToStorageLock); + } + + @Override + public void sendLogEntryToAlertBatch(List<LogEntry> logEntries) { + if (logEntries == null || logEntries.isEmpty()) { + return; + } + if (logEntryProducer != null) { + try { + for (LogEntry logEntry : logEntries) { + ProducerRecord<Long, LogEntry> record = new ProducerRecord<>(kafka.getLogEntryDataTopic(), logEntry); + logEntryProducer.send(record); + } + } catch (Exception e) { + log.error("Failed to send LogEntry batch to Kafka: {}", e.getMessage()); + for (LogEntry logEntry : logEntries) { + logEntryQueue.offer(logEntry); + } + } + } else { + log.warn("logEntryProducer is not enabled, using memory queue"); + for (LogEntry logEntry : logEntries) { + logEntryQueue.offer(logEntry); + } + } + } + + @Override + public List<LogEntry> pollLogEntryToAlertBatch(int maxBatchSize) throws InterruptedException { + return genericBatchPollDataFunction(logEntryQueue, logEntryConsumer, logEntryLock, maxBatchSize); + } + + @Override + public void sendLogEntryToStorageBatch(List<LogEntry> logEntries) { + if (logEntries == null || logEntries.isEmpty()) { + return; + } + if (logEntryProducer != null) { + try { + for (LogEntry logEntry : logEntries) { + ProducerRecord<Long, LogEntry> record = new ProducerRecord<>(kafka.getLogEntryDataToStorageTopic(), logEntry); + logEntryProducer.send(record); + } + } catch (Exception e) { + log.error("Failed to send LogEntry batch to storage via Kafka: {}", e.getMessage()); + for (LogEntry logEntry : logEntries) { + logEntryToStorageQueue.offer(logEntry); + } + } + } else { + log.warn("logEntryProducer is not enabled, using memory queue for storage"); + for (LogEntry logEntry : logEntries) { + logEntryToStorageQueue.offer(logEntry); + } + } + } + + @Override + public List<LogEntry> pollLogEntryToStorageBatch(int maxBatchSize) throws InterruptedException { + return genericBatchPollDataFunction(logEntryToStorageQueue, logEntryToStorageConsumer, logEntryToStorageLock, maxBatchSize); + } + + public <T> List<T> genericBatchPollDataFunction(LinkedBlockingQueue<T> dataQueue, KafkaConsumer<Long, T> dataConsumer, + ReentrantLock lock, int maxBatchSize) throws InterruptedException { + List<T> batch = new ArrayList<>(maxBatchSize); + lock.lockInterruptibly(); + try { + dataQueue.drainTo(batch, maxBatchSize); + if (batch.size() >= maxBatchSize) { + return batch; + } + ConsumerRecords<Long, T> records = dataConsumer.poll(Duration.ofSeconds(1)); + for (ConsumerRecord<Long, T> record : records) { + if (batch.size() < maxBatchSize) { + batch.add(record.value()); + } else { + dataQueue.offer(record.value()); + } + } + dataConsumer.commitAsync(); + } catch (Exception e) { + log.error(e.getMessage()); + } finally { + lock.unlock(); + } + return batch; + } + + @Override + public void destroy() throws Exception { + if (metricsDataProducer != null) { + metricsDataProducer.close(); + } + if (metricsDataToAlertConsumer != null) { + metricsDataToAlertConsumer.close(); + } + if (metricsDataToStorageConsumer != null) { + metricsDataToStorageConsumer.close(); + } + if (serviceDiscoveryDataConsumer != null) { + serviceDiscoveryDataConsumer.close(); + } + if (logEntryProducer != null) { + logEntryProducer.close(); + } + if (logEntryConsumer != null) { + logEntryConsumer.close(); + } + if (logEntryToStorageConsumer != null) { + logEntryToStorageConsumer.close(); + } + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/queue/impl/RedisCommonDataQueue.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/queue/impl/RedisCommonDataQueue.java new file mode 100644 index 0000000..49c187a --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/queue/impl/RedisCommonDataQueue.java @@ -0,0 +1,242 @@ +/* + * 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.queue.impl; + +import java.util.ArrayList; +import java.util.List; +import io.lettuce.core.KeyValue; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.config.CommonProperties; +import org.apache.hertzbeat.common.constants.DataQueueConstants; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.common.serialize.RedisLogEntryCodec; +import org.apache.hertzbeat.common.serialize.RedisMetricsDataCodec; +import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; + +import java.util.Objects; + +/** + * common data queue implement redis. + */ +@Slf4j +@Configuration +@ConditionalOnProperty( + prefix = DataQueueConstants.PREFIX, + name = DataQueueConstants.NAME, + havingValue = DataQueueConstants.REDIS +) +public class RedisCommonDataQueue implements CommonDataQueue, DisposableBean { + + private final RedisClient redisClient; + private final StatefulRedisConnection<String, CollectRep.MetricsData> connection; + private final RedisCommands<String, CollectRep.MetricsData> syncCommands; + private final StatefulRedisConnection<String, LogEntry> logEntryConnection; + private final RedisCommands<String, LogEntry> logEntrySyncCommands; + private final String metricsDataQueueNameToStorage; + private final String metricsDataQueueNameForServiceDiscovery; + private final String metricsDataQueueNameToAlerter; + private final String logEntryQueueName; + private final String logEntryToStorageQueueName; + private final CommonProperties.RedisProperties redisProperties; + private final Long waitTimeout; + + public RedisCommonDataQueue(CommonProperties properties) { + + if (properties == null || properties.getQueue() == null || properties.getQueue().getRedis() == null) { + log.error("init error, please config common.queue.redis props in application.yml"); + throw new IllegalArgumentException("please config common.queue.redis props"); + } + + this.redisProperties = properties.getQueue().getRedis(); + + this.redisClient = RedisClient.create( + RedisURI.builder() + .withHost(redisProperties.getRedisHost()) + .withPort(redisProperties.getRedisPort()) + .build() + ); + RedisMetricsDataCodec codec = new RedisMetricsDataCodec(); + this.connection = redisClient.connect(codec); + this.syncCommands = connection.sync(); + RedisLogEntryCodec logCodec = new RedisLogEntryCodec(); + this.logEntryConnection = redisClient.connect(logCodec); + this.logEntrySyncCommands = logEntryConnection.sync(); + this.metricsDataQueueNameToStorage = redisProperties.getMetricsDataQueueNameToPersistentStorage(); + this.metricsDataQueueNameForServiceDiscovery = redisProperties.getMetricsDataQueueNameForServiceDiscovery(); + this.metricsDataQueueNameToAlerter = redisProperties.getMetricsDataQueueNameToAlerter(); + this.logEntryQueueName = redisProperties.getLogEntryQueueName(); + this.logEntryToStorageQueueName = redisProperties.getLogEntryToStorageQueueName(); + this.waitTimeout = Objects.requireNonNullElse(redisProperties.getWaitTimeout(), 1L); + } + + @Override + public CollectRep.MetricsData pollMetricsDataToAlerter() throws InterruptedException { + return genericBlockingPollFunction(metricsDataQueueNameToAlerter, syncCommands); + } + + @Override + public CollectRep.MetricsData pollMetricsDataToStorage() throws InterruptedException { + return genericBlockingPollFunction(metricsDataQueueNameToStorage, syncCommands); + + } + + @Override + public CollectRep.MetricsData pollServiceDiscoveryData() throws InterruptedException { + return genericBlockingPollFunction(metricsDataQueueNameForServiceDiscovery, syncCommands); + } + + @Override + public void sendMetricsData(CollectRep.MetricsData metricsData) { + try { + syncCommands.lpush(metricsDataQueueNameToAlerter, metricsData); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + + @Override + public void sendMetricsDataToStorage(CollectRep.MetricsData metricsData) { + try { + syncCommands.lpush(metricsDataQueueNameToStorage, metricsData); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + + @Override + public void sendServiceDiscoveryData(CollectRep.MetricsData metricsData) { + try { + syncCommands.lpush(metricsDataQueueNameForServiceDiscovery, metricsData); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + + @Override + public void sendLogEntry(LogEntry logEntry) { + try { + logEntrySyncCommands.lpush(logEntryQueueName, logEntry); + } catch (Exception e) { + log.error("Failed to send LogEntry to Redis: {}", e.getMessage()); + } + } + + @Override + public LogEntry pollLogEntry() throws InterruptedException { + return genericBlockingPollFunction(logEntryQueueName, logEntrySyncCommands); + } + + @Override + public void sendLogEntryToStorage(LogEntry logEntry) { + try { + logEntrySyncCommands.lpush(logEntryToStorageQueueName, logEntry); + } catch (Exception e) { + log.error("Failed to send LogEntry to storage via Redis: {}", e.getMessage()); + } + } + + @Override + public LogEntry pollLogEntryToStorage() throws InterruptedException { + return genericBlockingPollFunction(logEntryToStorageQueueName, logEntrySyncCommands); + } + + @Override + @SuppressWarnings("unchecked") + public void sendLogEntryToAlertBatch(List<LogEntry> logEntries) { + if (logEntries == null || logEntries.isEmpty()) { + return; + } + try { + logEntrySyncCommands.lpush(logEntryQueueName, logEntries.toArray(new LogEntry[0])); + } catch (Exception e) { + log.error("Failed to send LogEntry batch to Redis: {}", e.getMessage()); + } + } + + @Override + public List<LogEntry> pollLogEntryToAlertBatch(int maxBatchSize) throws InterruptedException { + return genericBatchPollFunction(logEntryQueueName, logEntrySyncCommands, maxBatchSize); + } + + @Override + @SuppressWarnings("unchecked") + public void sendLogEntryToStorageBatch(List<LogEntry> logEntries) { + if (logEntries == null || logEntries.isEmpty()) { + return; + } + try { + logEntrySyncCommands.lpush(logEntryToStorageQueueName, logEntries.toArray(new LogEntry[0])); + } catch (Exception e) { + log.error("Failed to send LogEntry batch to storage via Redis: {}", e.getMessage()); + } + } + + @Override + public List<LogEntry> pollLogEntryToStorageBatch(int maxBatchSize) throws InterruptedException { + return genericBatchPollFunction(logEntryToStorageQueueName, logEntrySyncCommands, maxBatchSize); + } + + @Override + public void destroy() { + connection.close(); + logEntryConnection.close(); + redisClient.shutdown(); + } + + + + private <T> T genericBlockingPollFunction(String key, RedisCommands<String, T> commands) throws InterruptedException { + try { + // Use BRPOP for blocking pop with the configured timeout. + // If data arrives, it returns immediately; if it times out, it returns null. + KeyValue<String, T> keyData = commands.brpop(waitTimeout, key); + if (keyData != null) { + return keyData.getValue(); + } else { + // Returns null on timeout + return null; + } + } catch (Exception e) { + log.error("Redis BRPOP failed: {}", e.getMessage()); + throw new CommonDataQueueUnknownException(e.getMessage(), e); + } + } + + private List<LogEntry> genericBatchPollFunction(String key, RedisCommands<String, LogEntry> commands, int maxBatchSize) { + List<LogEntry> batch = new ArrayList<>(maxBatchSize); + try { + List<LogEntry> elements = commands.rpop(key, maxBatchSize); + if (elements != null) { + batch.addAll(elements); + } + } catch (Exception e) { + log.error("Redis batch poll failed: {}", e.getMessage()); + } + return batch; + } + +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/CommonThreadPool.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/CommonThreadPool.java new file mode 100644 index 0000000..bd6a42e --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/CommonThreadPool.java @@ -0,0 +1,123 @@ +/* + * 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.support; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.concurrent.BackgroundTaskExecutor; +import org.apache.hertzbeat.common.concurrent.ManagedExecutor; +import org.apache.hertzbeat.common.concurrent.ManagedExecutors; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * common task worker thread pool + */ +@Component +@Slf4j +public class CommonThreadPool implements BackgroundTaskExecutor, DisposableBean { + + private final ManagedExecutor workerExecutor; + + private final ManagedExecutor longRunningExecutor; + + public CommonThreadPool() { + this(VirtualThreadProperties.defaults()); + } + + @Autowired + public CommonThreadPool(VirtualThreadProperties virtualThreadProperties) { + VirtualThreadProperties properties = + virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties; + this.workerExecutor = createWorkerExecutor(properties); + this.longRunningExecutor = createLongRunningExecutor(properties, workerExecutor); + } + + private ManagedExecutor createWorkerExecutor(VirtualThreadProperties properties) { + Thread.UncaughtExceptionHandler handler = (thread, throwable) -> { + log.error("common executor has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }; + if (properties.enabled()) { + VirtualThreadProperties.PoolProperties poolProperties = properties.common(); + return ManagedExecutors.newVirtualExecutor("common-worker", "common-worker-", + poolProperties.mode(), poolProperties.maxConcurrentJobs(), handler); + } + return ManagedExecutors.wrap("common-worker", createLegacyExecutor(handler)); + } + + private ManagedExecutor createLongRunningExecutor(VirtualThreadProperties properties, ManagedExecutor fallback) { + if (!properties.enabled()) { + return fallback; + } + return ManagedExecutors.newPlatformExecutor("common-long-running", "common-long-running-", + (thread, throwable) -> { + log.error("common longRunningExecutor has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }); + } + + private ExecutorService createLegacyExecutor(Thread.UncaughtExceptionHandler handler) { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler(handler) + .setDaemon(true) + .setNameFormat("common-worker-%d") + .build(); + return new ThreadPoolExecutor(1, + Integer.MAX_VALUE, + 10, + TimeUnit.SECONDS, + new SynchronousQueue<>(), + threadFactory, + new ThreadPoolExecutor.AbortPolicy()); + } + + /** + * Run the task thread + * @param runnable Task + * @throws RejectedExecutionException when thread pool full + */ + public void execute(Runnable runnable) throws RejectedExecutionException { + workerExecutor.execute(runnable); + } + + /** + * Run a long-lived task outside of the short-task execution lane. + * + * @param runnable task + */ + public void executeLongRunning(Runnable runnable) { + longRunningExecutor.execute(runnable); + } + + @Override + public void destroy() throws Exception { + workerExecutor.close(); + if (longRunningExecutor != workerExecutor) { + longRunningExecutor.close(); + } + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/SpringContextHolder.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/SpringContextHolder.java new file mode 100644 index 0000000..432a7aa --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/SpringContextHolder.java @@ -0,0 +1,82 @@ +/* + * 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.support; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +/** + * Spring ApplicationContext Holder + */ +@Component +public class SpringContextHolder implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + private static ConfigurableApplicationContext configurableApplicationContext; + + @Override + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { + set(applicationContext); + if (applicationContext instanceof ConfigurableApplicationContext context) { + configurableApplicationContext = context; + } + } + + private static void set(ApplicationContext applicationContext) { + SpringContextHolder.applicationContext = applicationContext; + } + + public static ApplicationContext getApplicationContext() { + assertApplicationContext(); + return applicationContext; + } + + @SuppressWarnings("unchecked") + public static <T> T getBean(String beanName) { + assertApplicationContext(); + return (T) applicationContext.getBean(beanName); + } + + public static <T> T getBean(Class<T> clazz) { + assertApplicationContext(); + return (T) applicationContext.getBean(clazz); + } + + public static void shutdown() { + assertApplicationContext(); + configurableApplicationContext.close(); + } + + public static boolean isActive() { + if (configurableApplicationContext == null) { + return false; + } + return configurableApplicationContext.isActive(); + } + + private static void assertApplicationContext() { + if (null == applicationContext || null == configurableApplicationContext) { + throw new RuntimeException("applicationContext is null, please inject the springContextHolder"); + } + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/event/AiProviderConfigChangeEvent.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/event/AiProviderConfigChangeEvent.java new file mode 100644 index 0000000..f539fb7 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/event/AiProviderConfigChangeEvent.java @@ -0,0 +1,31 @@ +/* + * 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.support.event; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; + +/** + * Ai Provider configuration change event + */ +public class AiProviderConfigChangeEvent extends ApplicationEvent { + + public AiProviderConfigChangeEvent(ApplicationContext source) { + super(source); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/event/MonitorDeletedEvent.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/event/MonitorDeletedEvent.java new file mode 100644 index 0000000..a451aa9 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/event/MonitorDeletedEvent.java @@ -0,0 +1,40 @@ +/* + * 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.support.event; + +import org.springframework.context.ApplicationEvent; + +/** + * the event for monitor delete + */ +public class MonitorDeletedEvent extends ApplicationEvent { + + /** + * monitoring id + */ + private final Long monitorId; + + public MonitorDeletedEvent(Object source, Long monitorId) { + super(source); + this.monitorId = monitorId; + } + + public Long getMonitorId() { + return monitorId; + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/event/SmsConfigChangeEvent.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/event/SmsConfigChangeEvent.java new file mode 100644 index 0000000..2c8ca46 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/event/SmsConfigChangeEvent.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.support.event; + +import org.springframework.context.ApplicationEvent; + +/** + * the event for sms config change + */ +public class SmsConfigChangeEvent extends ApplicationEvent { + + public SmsConfigChangeEvent(Object source) { + super(source); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/event/SystemConfigChangeEvent.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/event/SystemConfigChangeEvent.java new file mode 100644 index 0000000..5d4e25d --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/event/SystemConfigChangeEvent.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.support.event; + +import org.springframework.context.ApplicationEvent; + +/** + * the event for system config change + */ +public class SystemConfigChangeEvent extends ApplicationEvent { + + public SystemConfigChangeEvent(Object source) { + super(source); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/EmailParamValidator.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/EmailParamValidator.java new file mode 100644 index 0000000..50d74eb --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/EmailParamValidator.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.support.valid; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * email validator + */ +public class EmailParamValidator implements ConstraintValidator<EmailValid, String> { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + // validate email + return CommonUtil.validateEmail(value); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/EmailValid.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/EmailValid.java new file mode 100644 index 0000000..36dd6b5 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/EmailValid.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.support.valid; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * EmailValid + */ +@Target({ FIELD, PARAMETER }) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = EmailParamValidator.class) +public @interface EmailValid { + + String message() default "Email value is invalid"; + + Class<?>[] groups() default {}; + + Class<? extends Payload>[] payload() default {}; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/HostParamValidator.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/HostParamValidator.java new file mode 100644 index 0000000..5a5a93f --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/HostParamValidator.java @@ -0,0 +1,64 @@ +/* + * 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.support.valid; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.springframework.util.StringUtils; + +/** + * Host Param Validator + */ +public class HostParamValidator implements ConstraintValidator<HostValid, String> { + public static final String HTTP = "http://"; + public static final String HTTPS = "https://"; + public static final String BLANK = ""; + public static final String PATTERN_HTTP = "(?i)http://"; + public static final String PATTERN_HTTPS = "(?i)https://"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (!StringUtils.hasText(value)) { + return true; + } + if (value.toLowerCase().contains(HTTP)){ + value = value.replaceFirst(PATTERN_HTTP, BLANK); + } + if (value.toLowerCase().contains(HTTPS)){ + value = value.replaceFirst(PATTERN_HTTPS, BLANK); + } + + String hostPart = value; + + if (value.contains(":")) { + // if contains multiple ":", it may be IPv6 with port + if (value.lastIndexOf(":") > value.indexOf(":") && value.contains("[")) { + int portIndex = value.lastIndexOf(":"); + hostPart = value.substring(0, portIndex); + } else if (value.split(":").length == 2) { + // it is IPv4 or domain with port + String[] parts = value.split(":"); + hostPart = parts[0]; + } + } + + return IpDomainUtil.validateIpDomain(hostPart); + } + +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/HostValid.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/HostValid.java new file mode 100644 index 0000000..14065f6 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/HostValid.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.support.valid; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Host Param Validator + */ +@Target({ FIELD, PARAMETER }) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = HostParamValidator.class) +public @interface HostValid { + + String message() default "Host need ipv4,ipv6,hostname or domain,<br>EG:127.0.0.1 hertzbeat.com"; + + Class<?>[] groups() default {}; + + Class<? extends Payload>[] payload() default {}; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/PhoneNumParamValidator.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/PhoneNumParamValidator.java new file mode 100644 index 0000000..c8a27bf --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/PhoneNumParamValidator.java @@ -0,0 +1,33 @@ +/* + * 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.support.valid; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.apache.hertzbeat.common.util.CommonUtil; + +/** + * PhoneNum Param Validator + */ +public class PhoneNumParamValidator implements ConstraintValidator<PhoneNumValid, String> { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return CommonUtil.validatePhoneNum(value); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/PhoneNumValid.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/PhoneNumValid.java new file mode 100644 index 0000000..3a16252 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/PhoneNumValid.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.support.valid; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * PhoneNum Param Validator + */ +@Target({ FIELD, PARAMETER }) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = PhoneNumParamValidator.class) +public @interface PhoneNumValid { + + String message() default "Phone num value is invalid"; + + Class<?>[] groups() default {}; + + Class<? extends Payload>[] payload() default {}; +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityValidator.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityValidator.java new file mode 100644 index 0000000..6d58dff --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityValidator.java @@ -0,0 +1,150 @@ +/* + * 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.support.valid; + +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.LateralSubSelect; +import net.sf.jsqlparser.statement.select.ParenthesedSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SetOperationList; +import net.sf.jsqlparser.statement.select.WithItem; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * SQL Security Validator using JSqlParser 5.1+. + * Security Policy: + * 1. Only SELECT statements are allowed. + * 2. All referenced tables must be in the whitelist. + * 3. Subqueries, UNION, CTE, LATERAL are blocked. + */ +@Slf4j +public class SqlSecurityValidator { + + private final Set<String> allowedTables; + + public SqlSecurityValidator(Collection<String> allowedTables) { + if (CollectionUtils.isEmpty(allowedTables)) { + this.allowedTables = new HashSet<>(); + } else { + this.allowedTables = allowedTables.stream() + .map(this::normalizeIdentifier) + .collect(Collectors.toSet()); + } + } + + public void validate(String sql) throws SqlSecurityException { + if (sql == null || sql.trim().isEmpty()) { + throw new SqlSecurityException("SQL statement cannot be empty"); + } + + Statement statement; + try { + statement = CCJSqlParserUtil.parse(sql); + } catch (JSQLParserException e) { + log.warn("Failed to parse SQL: {}", sql, e); + throw new SqlSecurityException("Invalid SQL syntax: " + e.getMessage(), e); + } + + if (!(statement instanceof Select select)) { + throw new SqlSecurityException("Only SELECT statements are allowed."); + } + + // Check for CTE at top level + if (select.getWithItemsList() != null && !select.getWithItemsList().isEmpty()) { + throw new SqlSecurityException("CTE (WITH clause) is not allowed"); + } + + // Use custom TablesNamesFinder that throws on dangerous structures + SecurityTablesNamesFinder finder = new SecurityTablesNamesFinder(); + List<String> tables; + try { + tables = finder.getTableList(statement); + } catch (SecurityViolationException e) { + throw new SqlSecurityException(e.getMessage()); + } + + validateTables(tables); + } + + private void validateTables(List<String> tables) throws SqlSecurityException { + if (CollectionUtils.isEmpty(tables)) { + return; + } + if (allowedTables.isEmpty()) { + throw new SqlSecurityException("No access allowed: whitelist is empty."); + } + + for (String table : tables) { + String normalizedTable = normalizeIdentifier(table); + if (!allowedTables.contains(normalizedTable)) { + throw new SqlSecurityException("Access to table '" + table + "' is not allowed. " + + "Allowed tables: " + allowedTables); + } + } + } + + private String normalizeIdentifier(String identifier) { + if (identifier == null) { + return ""; + } + return identifier.replace("\"", "").replace("`", "").replace("'", "").toLowerCase(); + } + + private static class SecurityViolationException extends RuntimeException { + SecurityViolationException(String message) { + super(message); + } + } + + /** + * Custom TablesNamesFinder that throws exceptions on dangerous SQL structures. + * Extends TablesNamesFinder with proper generic type to avoid raw type warnings. + */ + private static class SecurityTablesNamesFinder extends TablesNamesFinder<Void> { + + @Override + public Void visit(ParenthesedSelect parenthesedSelect, Object context) { + throw new SecurityViolationException("Subqueries are not allowed"); + } + + @Override + public Void visit(SetOperationList setOpList, Object context) { + throw new SecurityViolationException("UNION and set operations are not allowed"); + } + + @Override + public Void visit(LateralSubSelect lateralSubSelect, Object context) { + throw new SecurityViolationException("LATERAL subqueries are not allowed"); + } + + @Override + public Void visit(WithItem withItem, Object context) { + throw new SecurityViolationException("CTE (WITH clause) is not allowed"); + } + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/util/FileUtil.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/util/FileUtil.java new file mode 100644 index 0000000..d0ac10b --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/util/FileUtil.java @@ -0,0 +1,80 @@ +/* + * 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.util; + +import java.util.Map; +import org.apache.hertzbeat.common.constants.ExportFileConstants; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +/** + * File utils. + */ + +public final class FileUtil { + + private FileUtil() { + } + + private static final Map<String, String> fileTypes; + + static { + fileTypes = Map.of( + ExportFileConstants.JsonFile.FILE_SUFFIX, ExportFileConstants.JsonFile.TYPE, + ExportFileConstants.YamlFile.FILE_SUFFIX, ExportFileConstants.YamlFile.TYPE, + ExportFileConstants.ExcelFile.FILE_SUFFIX, ExportFileConstants.ExcelFile.TYPE + ); + } + + /** + * Get file name. + * @param file {@link MultipartFile} + * @return file name + */ + public static String getFileName(MultipartFile file) { + + var fileName = file.getOriginalFilename(); + if (!StringUtils.hasText(fileName)) { + return ""; + } + return fileName; + } + + /** + * Get file type. + * @param file {@link MultipartFile} + * @return file type + */ + public static String getFileType(MultipartFile file) { + + var fileName = getFileName(file); + + if (!StringUtils.hasText(fileName)) { + return ""; + } + + var dotIndex = fileName.lastIndexOf('.'); + if (dotIndex == -1 || dotIndex == fileName.length() - 1) { + return ""; + } + var fileNameExtension = fileName.substring(dotIndex); + + return fileTypes.get(fileNameExtension); + } + +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/util/ResponseUtil.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/util/ResponseUtil.java new file mode 100644 index 0000000..fef61b4 --- /dev/null +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/util/ResponseUtil.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.util; + +import javax.naming.AuthenticationException; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.http.ResponseEntity; + +/** + * A tool which make the restful response be easy to use + */ +public class ResponseUtil { + public static <T, E extends Exception> ResponseEntity<Message<T>> handle(Supplier<T, E> supplier) { + try { + T result = supplier.get(); + return ResponseEntity.ok(Message.success(result)); + } catch (Exception e) { + byte err = CommonConstants.FAIL_CODE; + if (e.getClass().equals(AuthenticationException.class)) { + err = CommonConstants.LOGIN_FAILED_CODE; + } + return ResponseEntity.ok(Message.fail(err, e.getMessage())); + } + } + + public static <T, E extends Exception> ResponseEntity<Message<T>> handle(Runnable runner) { + try { + runner.run(); + return ResponseEntity.ok(Message.success()); + } catch (Exception e) { + byte err = CommonConstants.FAIL_CODE; + if (e.getClass().equals(AuthenticationException.class)) { + err = CommonConstants.LOGIN_FAILED_CODE; + } + return ResponseEntity.ok(Message.fail(err, e.getMessage())); + } + } + + /** + * Supplier interface for getting result + */ + public interface Supplier<T, E extends Exception> { + + /** + * Gets a result. + * + * @return a result + */ + T get() throws E; + } + + /** + * Runnable interface for running + */ + public interface Runnable { + + /** + * Run target method. + */ + void run() throws Exception; + } +} diff --git a/hertzbeat-common-spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hertzbeat-common-spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..4130e4c --- /dev/null +++ b/hertzbeat-common-spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,16 @@ +# 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. + +org.apache.hertzbeat.common.config.CommonConfig \ No newline at end of file diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/cache/CacheFactoryTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/cache/CacheFactoryTest.java new file mode 100644 index 0000000..f5613e3 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/cache/CacheFactoryTest.java @@ -0,0 +1,41 @@ +/* + * 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.cache; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.LinkedList; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link CacheFactory} + */ +public class CacheFactoryTest { + @Test + void common() { + CacheFactory.setNoticeCache(new LinkedList<>()); + CacheFactory.setAlertSilenceCache(new LinkedList<>()); + assertNotNull(CacheFactory.getAlertSilenceCache()); + assertNotNull(CacheFactory.getNoticeCache()); + CacheFactory.clearNoticeCache(); + CacheFactory.clearAlertSilenceCache(); + assertNull(CacheFactory.getAlertSilenceCache()); + assertNull(CacheFactory.getNoticeCache()); + } +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/cache/CaffeineCacheTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/cache/CaffeineCacheTest.java new file mode 100644 index 0000000..b60570d --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/cache/CaffeineCacheTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.cache; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.time.Duration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link CaffeineCacheServiceImpl} + */ +class CaffeineCacheTest { + private CommonCacheService<String, String> cacheService; + + @BeforeEach + void setUp() { + cacheService = new CaffeineCacheServiceImpl<>(10, 100, Duration.ofMillis(3000), false); + } + + @Test + void testCache() throws InterruptedException { + String key = "key"; + String value = "value"; + + // test get & put + cacheService.put(key, value); + Assertions.assertEquals(value, cacheService.get(key)); + Assertions.assertTrue(cacheService.containsKey(key)); + + // test remove + cacheService.remove(key); + Assertions.assertNull(cacheService.get(key)); + + // test expire time + cacheService.put(key, value); + Thread.sleep(3000); + Assertions.assertNull(cacheService.get(key)); + Assertions.assertNull(cacheService.get(key)); + + // test clear + for (int i = 0; i < 10; i++) { + cacheService.put(key + i, value); + } + cacheService.clear(); + for (int i = 0; i < 10; i++) { + Assertions.assertNull(cacheService.get(key + i)); + } + // test new method : cacheService.putAndGetOld(key,newValue) + String oldValue = "oldOne"; + String newValue = "newOne"; + cacheService.put(key, oldValue); + Assertions.assertEquals(oldValue, cacheService.putAndGetOld(key, newValue)); + Assertions.assertEquals(newValue, cacheService.get(key)); + } + + @Test + void weekCache() { + CommonCacheService<String, String> cache = new CaffeineCacheServiceImpl<>(10, 100, Duration.ofMillis(3000), true); + assertNotNull(cache); + } +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/config/SmsConfigBindingTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/config/SmsConfigBindingTest.java new file mode 100644 index 0000000..638b7e1 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/config/SmsConfigBindingTest.java @@ -0,0 +1,50 @@ +/* + * 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.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig; +import org.junit.jupiter.api.Test; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class SmsConfigBindingTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(BindingConfig.class); + + @Test + void bindsRuntimeSmsConfigWithoutSpringAnnotationsOnModel() { + contextRunner.withPropertyValues( + "alerter.sms.enable=true", + "alerter.sms.type=smslocal", + "alerter.sms.smslocal.api-key=test-key") + .run(context -> { + SmsConfig smsConfig = context.getBean(SmsConfig.class); + assertTrue(smsConfig.isEnable()); + assertEquals("smslocal", smsConfig.getType()); + assertEquals("test-key", smsConfig.getSmslocal().getApiKey()); + }); + } + + @EnableConfigurationProperties(SmsConfigBinding.class) + static class BindingConfig { + } +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/config/VirtualThreadPropertiesTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/config/VirtualThreadPropertiesTest.java new file mode 100644 index 0000000..615e7ff --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/config/VirtualThreadPropertiesTest.java @@ -0,0 +1,114 @@ +/* + * 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.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hertzbeat.common.concurrent.AdmissionMode; +import org.junit.jupiter.api.Test; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; + +class VirtualThreadPropertiesTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(BindingConfig.class); + + @Test + void defaultsRemainSafeWithoutExternalConfiguration() { + VirtualThreadProperties properties = VirtualThreadProperties.defaults(); + + assertTrue(properties.enabled()); + + assertEquals(AdmissionMode.LIMIT_AND_REJECT, properties.collector().mode()); + assertEquals(512, properties.collector().maxConcurrentJobs()); + + assertEquals(AdmissionMode.UNBOUNDED_VT, properties.common().mode()); + assertEquals(AdmissionMode.LIMIT_AND_REJECT, properties.manager().mode()); + assertEquals(10, properties.manager().maxConcurrentJobs()); + + assertEquals(AdmissionMode.LIMIT_AND_REJECT, properties.alerter().notifyPool().mode()); + assertEquals(64, properties.alerter().notifyPool().maxConcurrentJobs()); + assertEquals(10, properties.alerter().periodicMaxConcurrentJobs()); + assertEquals(10, properties.alerter().logWorker().maxConcurrentJobs()); + assertEquals(1000, properties.alerter().logWorker().queueCapacity()); + assertEquals(2, properties.alerter().reduce().maxConcurrentJobs()); + assertEquals(0, properties.alerter().reduce().queueCapacity()); + assertEquals(2, properties.alerter().windowEvaluator().maxConcurrentJobs()); + assertEquals(0, properties.alerter().windowEvaluator().queueCapacity()); + assertEquals(4, properties.alerter().notifyMaxConcurrentPerChannel()); + + assertEquals(AdmissionMode.UNBOUNDED_VT, properties.warehouse().mode()); + + assertTrue(properties.async().enabled()); + assertEquals(256, properties.async().concurrencyLimit()); + assertTrue(properties.async().rejectWhenLimitReached()); + assertEquals(5000L, properties.async().taskTerminationTimeout()); + } + + @Test + void collectorModeOnlyBindingRetainsDefaultConcurrency() { + contextRunner.withPropertyValues("hertzbeat.vthreads.collector.mode=LIMIT_AND_REJECT") + .run(context -> { + VirtualThreadProperties properties = context.getBean(VirtualThreadProperties.class); + assertEquals(AdmissionMode.LIMIT_AND_REJECT, properties.collector().mode()); + assertEquals(512, properties.collector().maxConcurrentJobs()); + }); + } + + @Test + void collectorModeOverrideUsesConfiguredMode() { + contextRunner.withPropertyValues("hertzbeat.vthreads.collector.mode=LIMIT_AND_BLOCK") + .run(context -> { + VirtualThreadProperties properties = context.getBean(VirtualThreadProperties.class); + assertEquals(AdmissionMode.LIMIT_AND_BLOCK, properties.collector().mode()); + assertEquals(512, properties.collector().maxConcurrentJobs()); + }); + } + + @Test + void notifyModeOnlyBindingRetainsDefaultConcurrency() { + contextRunner.withPropertyValues("hertzbeat.vthreads.alerter.notify.mode=LIMIT_AND_BLOCK") + .run(context -> { + VirtualThreadProperties properties = context.getBean(VirtualThreadProperties.class); + assertEquals(AdmissionMode.LIMIT_AND_BLOCK, properties.alerter().notifyPool().mode()); + assertEquals(64, properties.alerter().notifyPool().maxConcurrentJobs()); + }); + } + + @Test + void logWorkerQueueOnlyBindingRetainsDefaultConcurrency() { + contextRunner.withPropertyValues("hertzbeat.vthreads.alerter.log-worker.queue-capacity=32") + .run(context -> { + VirtualThreadProperties properties = context.getBean(VirtualThreadProperties.class); + assertEquals(10, properties.alerter().logWorker().maxConcurrentJobs()); + assertEquals(32, properties.alerter().logWorker().queueCapacity()); + }); + } + + @EnableConfigurationProperties(VirtualThreadPropertiesBinding.class) + static class BindingConfig { + + @Bean + VirtualThreadProperties virtualThreadProperties(VirtualThreadPropertiesBinding binding) { + return binding.toRuntimeProperties(); + } + } +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/entity/manager/MetricsFavoriteTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/entity/manager/MetricsFavoriteTest.java new file mode 100644 index 0000000..2830015 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/entity/manager/MetricsFavoriteTest.java @@ -0,0 +1,323 @@ +/* + * 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.manager; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.Set; + +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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test case for {@link MetricsFavorite} + */ +class MetricsFavoriteTest { + + private final ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + private final Validator validator = factory.getValidator(); + + @Test + void testBuilder() { + String creator = "testUser"; + Long monitorId = 1L; + String metricsName = "cpu"; + LocalDateTime createTime = LocalDateTime.now(); + + MetricsFavorite favorite = MetricsFavorite.builder() + .id(1L) + .creator(creator) + .monitorId(monitorId) + .metricsName(metricsName) + .createTime(createTime) + .build(); + + assertNotNull(favorite); + assertEquals(1L, favorite.getId()); + assertEquals(creator, favorite.getCreator()); + assertEquals(monitorId, favorite.getMonitorId()); + assertEquals(metricsName, favorite.getMetricsName()); + assertEquals(createTime, favorite.getCreateTime()); + } + + @Test + void testBuilderWithoutOptionalFields() { + String creator = "testUser"; + Long monitorId = 1L; + String metricsName = "cpu"; + + MetricsFavorite favorite = MetricsFavorite.builder() + .creator(creator) + .monitorId(monitorId) + .metricsName(metricsName) + .build(); + + assertNotNull(favorite); + assertNull(favorite.getId()); + assertEquals(creator, favorite.getCreator()); + assertEquals(monitorId, favorite.getMonitorId()); + assertEquals(metricsName, favorite.getMetricsName()); + assertNull(favorite.getCreateTime()); + } + + @Test + void testDefaultConstructor() { + MetricsFavorite favorite = new MetricsFavorite(); + + assertNotNull(favorite); + assertNull(favorite.getId()); + assertNull(favorite.getCreator()); + assertNull(favorite.getMonitorId()); + assertNull(favorite.getMetricsName()); + assertNull(favorite.getCreateTime()); + } + + @Test + void testSettersAndGetters() { + MetricsFavorite favorite = new MetricsFavorite(); + String creator = "testUser"; + Long monitorId = 1L; + String metricsName = "cpu"; + LocalDateTime createTime = LocalDateTime.now(); + + favorite.setId(1L); + favorite.setCreator(creator); + favorite.setMonitorId(monitorId); + favorite.setMetricsName(metricsName); + favorite.setCreateTime(createTime); + + assertEquals(1L, favorite.getId()); + assertEquals(creator, favorite.getCreator()); + assertEquals(monitorId, favorite.getMonitorId()); + assertEquals(metricsName, favorite.getMetricsName()); + assertEquals(createTime, favorite.getCreateTime()); + } + + @Test + void testValidation_ValidEntity() { + MetricsFavorite favorite = MetricsFavorite.builder() + .creator("testUser") + .monitorId(1L) + .metricsName("cpu") + .createTime(LocalDateTime.now()) + .build(); + + Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite); + + assertTrue(violations.isEmpty()); + } + + @Test + void testValidation_NullCreator() { + MetricsFavorite favorite = MetricsFavorite.builder() + .creator(null) + .monitorId(1L) + .metricsName("cpu") + .createTime(LocalDateTime.now()) + .build(); + + Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite); + + assertFalse(violations.isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("creator"))); + } + + @Test + void testValidation_BlankCreator() { + MetricsFavorite favorite = MetricsFavorite.builder() + .creator(" ") + .monitorId(1L) + .metricsName("cpu") + .createTime(LocalDateTime.now()) + .build(); + + Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite); + + assertFalse(violations.isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("creator"))); + } + + @Test + void testValidation_CreatorTooLong() { + String longCreator = "a".repeat(256); + MetricsFavorite favorite = MetricsFavorite.builder() + .creator(longCreator) + .monitorId(1L) + .metricsName("cpu") + .createTime(LocalDateTime.now()) + .build(); + + Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite); + + assertFalse(violations.isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("creator"))); + } + + @Test + void testValidation_NullMonitorId() { + MetricsFavorite favorite = MetricsFavorite.builder() + .creator("testUser") + .monitorId(null) + .metricsName("cpu") + .createTime(LocalDateTime.now()) + .build(); + + Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite); + + assertFalse(violations.isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("monitorId"))); + } + + @Test + void testValidation_NullMetricsName() { + MetricsFavorite favorite = MetricsFavorite.builder() + .creator("testUser") + .monitorId(1L) + .metricsName(null) + .createTime(LocalDateTime.now()) + .build(); + + Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite); + + assertFalse(violations.isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("metricsName"))); + } + + @Test + void testValidation_BlankMetricsName() { + MetricsFavorite favorite = MetricsFavorite.builder() + .creator("testUser") + .monitorId(1L) + .metricsName(" ") + .createTime(LocalDateTime.now()) + .build(); + + Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite); + + assertFalse(violations.isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("metricsName"))); + } + + @Test + void testValidation_MetricsNameTooLong() { + String longMetricsName = "a".repeat(256); + MetricsFavorite favorite = MetricsFavorite.builder() + .creator("testUser") + .monitorId(1L) + .metricsName(longMetricsName) + .createTime(LocalDateTime.now()) + .build(); + + Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite); + + assertFalse(violations.isEmpty()); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("metricsName"))); + } + + @Test + void testEqualsAndHashCode() { + LocalDateTime now = LocalDateTime.now(); + MetricsFavorite favorite1 = MetricsFavorite.builder() + .id(1L) + .creator("testUser") + .monitorId(1L) + .metricsName("cpu") + .createTime(now) + .build(); + + MetricsFavorite favorite2 = MetricsFavorite.builder() + .id(1L) + .creator("testUser") + .monitorId(1L) + .metricsName("cpu") + .createTime(now) + .build(); + + MetricsFavorite favorite3 = MetricsFavorite.builder() + .id(2L) + .creator("testUser") + .monitorId(1L) + .metricsName("cpu") + .createTime(now) + .build(); + + assertEquals(favorite1, favorite2); + assertEquals(favorite1.hashCode(), favorite2.hashCode()); + assertNotEquals(favorite1, favorite3); + assertNotEquals(favorite1.hashCode(), favorite3.hashCode()); + } + + @Test + void testToString() { + MetricsFavorite favorite = MetricsFavorite.builder() + .id(1L) + .creator("testUser") + .monitorId(1L) + .metricsName("cpu") + .createTime(LocalDateTime.now()) + .build(); + + String toString = favorite.toString(); + + assertNotNull(toString); + assertTrue(toString.contains("MetricsFavorite")); + assertTrue(toString.contains("testUser")); + assertTrue(toString.contains("cpu")); + } + + @Test + void testCreatorMaxLength() { + String maxLengthCreator = "a".repeat(255); + MetricsFavorite favorite = MetricsFavorite.builder() + .creator(maxLengthCreator) + .monitorId(1L) + .metricsName("cpu") + .createTime(LocalDateTime.now()) + .build(); + + Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite); + + assertTrue(violations.isEmpty()); + assertEquals(255, favorite.getCreator().length()); + } + + @Test + void testMetricsNameMaxLength() { + String maxLengthMetricsName = "a".repeat(255); + MetricsFavorite favorite = MetricsFavorite.builder() + .creator("testUser") + .monitorId(1L) + .metricsName(maxLengthMetricsName) + .createTime(LocalDateTime.now()) + .build(); + + Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite); + + assertTrue(violations.isEmpty()); + assertEquals(255, favorite.getMetricsName().length()); + } +} \ No newline at end of file diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/queue/impl/InMemoryCommonDataQueueTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/queue/impl/InMemoryCommonDataQueueTest.java new file mode 100644 index 0000000..c8f0730 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/queue/impl/InMemoryCommonDataQueueTest.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.queue.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link InMemoryCommonDataQueue} + */ +class InMemoryCommonDataQueueTest { + + private InMemoryCommonDataQueue queue; + + @BeforeEach + void setUp() { + queue = new InMemoryCommonDataQueue(); + } + + @Test + void testMetricsData() throws InterruptedException { + + var metricsData = CollectRep.MetricsData.newBuilder().build(); + + queue.sendMetricsData(metricsData); + + CollectRep.MetricsData polledMetricsData = queue.pollMetricsDataToAlerter(); + + assertNotNull(polledMetricsData); + assertEquals(metricsData, polledMetricsData); + } + + @Test + void testLogEntry() throws InterruptedException { + + // Create a test log entry with comprehensive data + Map<String, Object> attributes = new HashMap<>(); + attributes.put("service.name", "hertzbeat"); + attributes.put("service.version", "1.0.0"); + + Map<String, Object> resource = new HashMap<>(); + resource.put("host.name", "localhost"); + resource.put("os.type", "linux"); + + LogEntry.InstrumentationScope scope = LogEntry.InstrumentationScope.builder() + .name("org.apache.hertzbeat.test") + .version("1.0.0") + .build(); + + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .observedTimeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .severityNumber(9) // INFO level + .severityText("INFO") + .body("Test log message for hertzbeat queue") + .attributes(attributes) + .resource(resource) + .instrumentationScope(scope) + .traceId("1234567890abcdef1234567890abcdef") + .spanId("1234567890abcdef") + .traceFlags(1) + .build(); + + // Test sending and polling log entry + queue.sendLogEntry(logEntry); + + LogEntry polledLogEntry = queue.pollLogEntry(); + + assertNotNull(polledLogEntry); + assertEquals(logEntry.getSeverityText(), polledLogEntry.getSeverityText()); + assertEquals(logEntry.getBody(), polledLogEntry.getBody()); + assertEquals(logEntry.getTraceId(), polledLogEntry.getTraceId()); + assertEquals(logEntry.getSpanId(), polledLogEntry.getSpanId()); + } + + @Test + void testLogEntryToStorage() throws InterruptedException { + + // Create a simple log entry for storage test + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .severityNumber(17) // ERROR level + .severityText("ERROR") + .body("Error log message for storage") + .build(); + + // Test sending and polling log entry to storage + queue.sendLogEntryToStorage(logEntry); + + LogEntry polledLogEntry = queue.pollLogEntryToStorage(); + + assertNotNull(polledLogEntry); + assertEquals(logEntry.getSeverityText(), polledLogEntry.getSeverityText()); + assertEquals(logEntry.getBody(), polledLogEntry.getBody()); + assertEquals(logEntry.getSeverityNumber(), polledLogEntry.getSeverityNumber()); + } + + @Test + void testGetQueueSizeMetricsInfo() { + + Map<String, Integer> metricsInfo = queue.getQueueSizeMetricsInfo(); + + assertEquals(0, metricsInfo.get("metricsDataToAlertQueue")); + assertEquals(0, metricsInfo.get("metricsDataToStorageQueue")); + assertEquals(0, metricsInfo.get("logEntryQueue")); + assertEquals(0, metricsInfo.get("logEntryToStorageQueue")); + + // Add metrics data and log entries + queue.sendMetricsData(CollectRep.MetricsData.newBuilder().build()); + queue.sendLogEntry(LogEntry.builder().body("Test log").build()); + queue.sendLogEntryToStorage(LogEntry.builder().body("Storage log").build()); + + metricsInfo = queue.getQueueSizeMetricsInfo(); + + assertEquals(1, metricsInfo.get("metricsDataToAlertQueue")); + assertEquals(0, metricsInfo.get("metricsDataToStorageQueue")); + assertEquals(1, metricsInfo.get("logEntryQueue")); + assertEquals(1, metricsInfo.get("logEntryToStorageQueue")); + } + + @Test + void testDestroy() { + + // Add both metrics data and log entries before destroy + queue.sendMetricsData(CollectRep.MetricsData.newBuilder().build()); + queue.sendLogEntry(LogEntry.builder().body("Test log").build()); + queue.sendLogEntryToStorage(LogEntry.builder().body("Storage log").build()); + + queue.destroy(); + + Map<String, Integer> metricsInfo = queue.getQueueSizeMetricsInfo(); + + assertEquals(0, metricsInfo.get("metricsDataToAlertQueue")); + assertEquals(0, metricsInfo.get("metricsDataToStorageQueue")); + assertEquals(0, metricsInfo.get("logEntryQueue")); + assertEquals(0, metricsInfo.get("logEntryToStorageQueue")); + } + +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/queue/impl/KafkaCommonDataQueueTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/queue/impl/KafkaCommonDataQueueTest.java new file mode 100644 index 0000000..5b7b22d --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/queue/impl/KafkaCommonDataQueueTest.java @@ -0,0 +1,260 @@ +/* + * 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.queue.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyCollection; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; +import java.lang.reflect.Field; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.hertzbeat.common.config.CommonProperties; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.common.TopicPartition; +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; + +/** + * Test case for {@link KafkaCommonDataQueue} + */ +@ExtendWith(MockitoExtension.class) +class KafkaCommonDataQueueTest { + + @Mock(lenient = true) + private KafkaConsumer<Long, CollectRep.MetricsData> metricsDataToAlertConsumer; + + @Mock(lenient = true) + private KafkaProducer<Long, CollectRep.MetricsData> metricsDataProducer; + + @Mock(lenient = true) + private KafkaProducer<Long, LogEntry> logEntryProducer; + + @Mock(lenient = true) + private KafkaConsumer<Long, LogEntry> logEntryConsumer; + + @Mock(lenient = true) + private KafkaConsumer<Long, LogEntry> logEntryToStorageConsumer; + + private KafkaCommonDataQueue kafkaCommonDataQueue; + + private CommonProperties commonProperties; + + @BeforeEach + void setUp() throws Exception { + commonProperties = mock(CommonProperties.class); + CommonProperties.DataQueueProperties dataQueueProperties = mock(CommonProperties.DataQueueProperties.class, withSettings().lenient()); + CommonProperties.KafkaProperties kafkaProperties = mock(CommonProperties.KafkaProperties.class, withSettings().lenient()); + + when(commonProperties.getQueue()).thenReturn(dataQueueProperties); + when(dataQueueProperties.getKafka()).thenReturn(kafkaProperties); + + // Set all required topics + when(kafkaProperties.getMetricsDataTopic()).thenReturn("metricsDataTopic"); + when(kafkaProperties.getLogEntryDataTopic()).thenReturn("logEntryDataTopic"); + when(kafkaProperties.getLogEntryDataToStorageTopic()).thenReturn("logEntryDataToStorageTopic"); + when(kafkaProperties.getAlertsDataTopic()).thenReturn("alertsDataTopic"); + when(kafkaProperties.getMetricsDataToStorageTopic()).thenReturn("metricsDataToStorageTopic"); + when(kafkaProperties.getServiceDiscoveryDataTopic()).thenReturn("serviceDiscoveryDataTopic"); + when(kafkaProperties.getServers()).thenReturn("localhost:9092"); + + // Simulate the subscribe method for consumers + doNothing().when(metricsDataToAlertConsumer).subscribe(anyCollection()); + doNothing().when(logEntryConsumer).subscribe(anyCollection()); + doNothing().when(logEntryToStorageConsumer).subscribe(anyCollection()); + + kafkaCommonDataQueue = new KafkaCommonDataQueue(commonProperties); + + // Use reflection to set private fields + setPrivateField(kafkaCommonDataQueue, "metricsDataProducer", metricsDataProducer); + setPrivateField(kafkaCommonDataQueue, "metricsDataToAlertConsumer", metricsDataToAlertConsumer); + setPrivateField(kafkaCommonDataQueue, "logEntryProducer", logEntryProducer); + setPrivateField(kafkaCommonDataQueue, "logEntryConsumer", logEntryConsumer); + setPrivateField(kafkaCommonDataQueue, "logEntryToStorageConsumer", logEntryToStorageConsumer); + } + + @Test + void testSendMetricsData() { + CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() + .setMetrics("test metrics") + .build(); + + kafkaCommonDataQueue.sendMetricsData(metricsData); + + verify(metricsDataProducer).send(any()); + } + + @Test + void testPollMetricsDataToAlerter() throws InterruptedException { + // Create a test data + CollectRep.MetricsData expectedData = CollectRep.MetricsData.newBuilder() + .setMetrics("test metrics") + .build(); + + // Create a ConsumerRecord containing test data + ConsumerRecord<Long, CollectRep.MetricsData> record = + new ConsumerRecord<>("metricsDataTopic", 0, 0L, 1L, expectedData); + + // Create a ConsumerRecords containing a single record. + Map<TopicPartition, List<ConsumerRecord<Long, CollectRep.MetricsData>>> recordsMap = + Collections.singletonMap( + new TopicPartition("metricsDataTopic", 0), + Collections.singletonList(record)); + ConsumerRecords<Long, CollectRep.MetricsData> records = new ConsumerRecords<>(recordsMap); + + when(metricsDataToAlertConsumer.poll(any(Duration.class))).thenReturn(records); + + CollectRep.MetricsData result = kafkaCommonDataQueue.pollMetricsDataToAlerter(); + assertEquals(expectedData, result); + + verify(metricsDataToAlertConsumer).commitAsync(); + } + + @Test + void testSendLogEntry() { + // Create a test log entry with comprehensive data + Map<String, Object> attributes = new HashMap<>(); + attributes.put("service.name", "hertzbeat"); + attributes.put("service.version", "1.0.0"); + + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .severityNumber(9) // INFO level + .severityText("INFO") + .body("Test log message for Kafka queue") + .attributes(attributes) + .traceId("1234567890abcdef1234567890abcdef") + .spanId("1234567890abcdef") + .build(); + + // Test sending log entry + kafkaCommonDataQueue.sendLogEntry(logEntry); + + // Verify that the producer was called + verify(logEntryProducer).send(any()); + } + + @Test + void testSendLogEntryToStorage() { + // Create a test log entry for storage + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .severityNumber(17) // ERROR level + .severityText("ERROR") + .body("Error log message for storage via Kafka") + .build(); + + // Test sending log entry to storage + kafkaCommonDataQueue.sendLogEntryToStorage(logEntry); + + // Verify that the producer was called + verify(logEntryProducer).send(any()); + } + + @Test + void testPollLogEntry() throws InterruptedException { + // Create test log entry + LogEntry expectedLogEntry = LogEntry.builder() + .timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .severityNumber(13) // WARN level + .severityText("WARN") + .body("Test warning log message") + .build(); + + // Create a ConsumerRecord containing test log entry + ConsumerRecord<Long, LogEntry> record = + new ConsumerRecord<>("logEntryDataTopic", 0, 0L, 1L, expectedLogEntry); + + // Create ConsumerRecords containing the log entry record + Map<TopicPartition, List<ConsumerRecord<Long, LogEntry>>> recordsMap = + Collections.singletonMap( + new TopicPartition("logEntryDataTopic", 0), + Collections.singletonList(record)); + ConsumerRecords<Long, LogEntry> records = new ConsumerRecords<>(recordsMap); + + when(logEntryConsumer.poll(any(Duration.class))).thenReturn(records); + + LogEntry result = kafkaCommonDataQueue.pollLogEntry(); + + assertEquals(expectedLogEntry, result); + verify(logEntryConsumer).commitAsync(); + } + + @Test + void testPollLogEntryToStorage() throws InterruptedException { + // Create test log entry for storage + LogEntry expectedLogEntry = LogEntry.builder() + .timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .severityNumber(21) // FATAL level + .severityText("FATAL") + .body("Critical error log for storage") + .build(); + + // Create a ConsumerRecord containing test log entry + ConsumerRecord<Long, LogEntry> record = + new ConsumerRecord<>("logEntryDataToStorageTopic", 0, 0L, 1L, expectedLogEntry); + + // Create ConsumerRecords containing the log entry record + Map<TopicPartition, List<ConsumerRecord<Long, LogEntry>>> recordsMap = + Collections.singletonMap( + new TopicPartition("logEntryDataToStorageTopic", 0), + Collections.singletonList(record)); + ConsumerRecords<Long, LogEntry> records = new ConsumerRecords<>(recordsMap); + + when(logEntryToStorageConsumer.poll(any(Duration.class))).thenReturn(records); + + LogEntry result = kafkaCommonDataQueue.pollLogEntryToStorage(); + + assertEquals(expectedLogEntry, result); + verify(logEntryToStorageConsumer).commitAsync(); + } + + @Test + void testDestroy() throws Exception { + kafkaCommonDataQueue.destroy(); + + // Verify that all producers and consumers are closed + verify(metricsDataToAlertConsumer).close(); + verify(metricsDataProducer).close(); + verify(logEntryProducer).close(); + verify(logEntryConsumer).close(); + verify(logEntryToStorageConsumer).close(); + } + + private void setPrivateField(Object object, String fieldName, Object value) throws Exception { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(object, value); + } +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/queue/impl/RedisCommonDataQueueTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/queue/impl/RedisCommonDataQueueTest.java new file mode 100644 index 0000000..ab02c20 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/queue/impl/RedisCommonDataQueueTest.java @@ -0,0 +1,204 @@ +/* + * 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.queue.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.KeyValue; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import org.apache.hertzbeat.common.config.CommonProperties; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.serialize.RedisLogEntryCodec; +import org.apache.hertzbeat.common.serialize.RedisMetricsDataCodec; +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.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * test for {@link RedisCommonDataQueue} + */ + +@ExtendWith(MockitoExtension.class) +class RedisCommonDataQueueTest { + + @Mock + private StatefulRedisConnection<String, CollectRep.MetricsData> connection; + @Mock + private StatefulRedisConnection<String, LogEntry> logEntryConnection; + + @Mock + private RedisCommands<String, CollectRep.MetricsData> syncCommands; + @Mock + private RedisCommands<String, LogEntry> logEntrySyncCommands; + + private RedisClient redisClient; + private CommonProperties commonProperties; + private CommonProperties.RedisProperties redisProperties; + private RedisCommonDataQueue redisCommonDataQueue; + + @BeforeEach + public void setUp() { + redisClient = mock(RedisClient.class); + commonProperties = mock(CommonProperties.class); + redisProperties = mock(CommonProperties.RedisProperties.class); + CommonProperties.DataQueueProperties dataQueueProperties = mock(CommonProperties.DataQueueProperties.class); + + when(commonProperties.getQueue()).thenReturn(dataQueueProperties); + when(dataQueueProperties.getRedis()).thenReturn(redisProperties); + when(redisProperties.getMetricsDataQueueNameToAlerter()).thenReturn("metricsDataQueueToAlerter"); + when(redisProperties.getLogEntryQueueName()).thenReturn("logEntryQueue"); + when(redisProperties.getLogEntryToStorageQueueName()).thenReturn("logEntryToStorageQueue"); + when(redisProperties.getRedisHost()).thenReturn("localhost"); + when(redisProperties.getRedisPort()).thenReturn(6379); + when(redisProperties.getWaitTimeout()).thenReturn(1L); + try (MockedStatic<RedisClient> mockedRedisClient = mockStatic(RedisClient.class)) { + mockedRedisClient.when(() -> RedisClient.create(any(RedisURI.class))).thenReturn(redisClient); + when(redisClient.connect(any(RedisMetricsDataCodec.class))).thenReturn(connection); + when(redisClient.connect(any(RedisLogEntryCodec.class))).thenReturn(logEntryConnection); + when(logEntryConnection.sync()).thenReturn(logEntrySyncCommands); + when(connection.sync()).thenReturn(syncCommands); + + redisCommonDataQueue = new RedisCommonDataQueue(commonProperties); + } + } + + @Test + public void testPollMetricsDataToAlerter() throws Exception { + CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() + .setMetrics("test metrics") + .build(); + String queueName = "metricsDataQueueToAlerter"; + + when(syncCommands.brpop(1L, queueName)).thenReturn(KeyValue.just(queueName, metricsData)); + + CollectRep.MetricsData actualMetricsData = redisCommonDataQueue.pollMetricsDataToAlerter(); + assertEquals(metricsData, actualMetricsData); + verify(syncCommands).brpop(1L, queueName); + } + + @Test + public void testSendMetricsData() throws Exception { + CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() + .setMetrics("test metrics") + .build(); + + redisCommonDataQueue.sendMetricsData(metricsData); + + verify(syncCommands).lpush("metricsDataQueueToAlerter", metricsData); + } + + @Test + public void testSendLogEntry() { + // Create a test log entry with comprehensive data + Map<String, Object> attributes = new HashMap<>(); + attributes.put("service.name", "hertzbeat"); + attributes.put("service.version", "1.0.0"); + + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .severityNumber(9) // INFO level + .severityText("INFO") + .body("Test log message for Redis queue") + .attributes(attributes) + .traceId("1234567890abcdef1234567890abcdef") + .spanId("1234567890abcdef") + .build(); + + // Test sending log entry + redisCommonDataQueue.sendLogEntry(logEntry); + + // Verify that the Redis commands were called + verify(logEntrySyncCommands).lpush("logEntryQueue", logEntry); + } + + @Test + public void testSendLogEntryToStorage() { + // Create a test log entry for storage + LogEntry logEntry = LogEntry.builder() + .timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .severityNumber(17) // ERROR level + .severityText("ERROR") + .body("Error log message for storage via Redis") + .build(); + + // Test sending log entry to storage + redisCommonDataQueue.sendLogEntryToStorage(logEntry); + + // Verify that the Redis commands were called + verify(logEntrySyncCommands).lpush("logEntryToStorageQueue", logEntry); + } + + @Test + public void testPollLogEntry() throws Exception { + // Create test log entry + LogEntry expectedLogEntry = LogEntry.builder() + .timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .severityNumber(13) // WARN level + .severityText("WARN") + .body("Test warning log message") + .build(); + String queueName = "logEntryQueue"; + + when(logEntrySyncCommands.brpop(1L, queueName)).thenReturn(KeyValue.just(queueName, expectedLogEntry)); + + LogEntry result = redisCommonDataQueue.pollLogEntry(); + + assertEquals(expectedLogEntry, result); + verify(logEntrySyncCommands).brpop(1L, "logEntryQueue"); + } + + @Test + public void testPollLogEntryToStorage() throws Exception { + // Create test log entry for storage + LogEntry expectedLogEntry = LogEntry.builder() + .timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L) + .severityNumber(21) // FATAL level + .severityText("FATAL") + .body("Critical error log for storage") + .build(); + String queueName = "logEntryToStorageQueue"; + + when(logEntrySyncCommands.brpop(1L, queueName)).thenReturn(KeyValue.just(queueName, expectedLogEntry)); + LogEntry result = redisCommonDataQueue.pollLogEntryToStorage(); + + assertEquals(expectedLogEntry, result); + verify(logEntrySyncCommands).brpop(1L, "logEntryToStorageQueue"); + } + + @Test + public void testDestroy() { + redisCommonDataQueue.destroy(); + verify(connection).close(); + verify(logEntryConnection).close(); + verify(redisClient).shutdown(); + } +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/CommonThreadPoolTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/CommonThreadPoolTest.java new file mode 100644 index 0000000..160eab4 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/CommonThreadPoolTest.java @@ -0,0 +1,108 @@ +/* + * 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.support; + +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.concurrent.CountDownLatch; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.hertzbeat.common.concurrent.AdmissionMode; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Test for {@link CommonThreadPool}. + */ +class CommonThreadPoolTest { + + private CommonThreadPool commonThreadPool; + + @AfterEach + void tearDown() throws Exception { + if (commonThreadPool != null) { + commonThreadPool.destroy(); + } + } + + @Test + void testExecuteRunsOnVirtualThread() throws Exception { + commonThreadPool = new CommonThreadPool(); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + + commonThreadPool.execute(() -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void testExecuteLongRunningRunsOnPlatformThread() throws Exception { + commonThreadPool = new CommonThreadPool(); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(true); + + commonThreadPool.executeLongRunning(() -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertFalse(virtualThread.get()); + } + + @Test + void testExecuteRejectsWhenConcurrencyLimitReached() throws Exception { + VirtualThreadProperties properties = new VirtualThreadProperties( + true, + VirtualThreadProperties.PoolProperties.collectorDefaults(), + new VirtualThreadProperties.PoolProperties(AdmissionMode.LIMIT_AND_REJECT, 1), + VirtualThreadProperties.PoolProperties.managerDefaults(), + VirtualThreadProperties.AlerterProperties.defaults(), + VirtualThreadProperties.PoolProperties.warehouseDefaults(), + VirtualThreadProperties.AsyncProperties.defaults()); + commonThreadPool = new CommonThreadPool(properties); + + CountDownLatch started = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + commonThreadPool.execute(() -> { + started.countDown(); + try { + release.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertTrue(started.await(5, TimeUnit.SECONDS)); + + try { + assertThrows(RejectedExecutionException.class, () -> commonThreadPool.execute(() -> { + })); + } finally { + release.countDown(); + } + } +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8ControlTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8ControlTest.java new file mode 100644 index 0000000..c30f5a2 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8ControlTest.java @@ -0,0 +1,73 @@ +/* + * 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.support; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import java.io.IOException; +import java.util.Locale; +import java.util.ResourceBundle; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link ResourceBundleUtf8Control} + */ +class ResourceBundleUtf8ControlTest { + + @Test + void testNewBundleWithPropertiesFormat() throws IllegalAccessException, InstantiationException, IOException { + + ResourceBundle.Control control = new ResourceBundleUtf8Control(); + ClassLoader loader = getClass().getClassLoader(); + String baseName = "msg"; + + ResourceBundle bundle = control.newBundle(baseName, Locale.ENGLISH, "java.properties", loader, false); + assertNotNull(bundle); + assertEquals("Hello, World!", bundle.getString("hello")); + + bundle = control.newBundle(baseName, Locale.ROOT, "java.properties", loader, false); + assertNotNull(bundle); + assertEquals("你好", bundle.getString("hello")); + } + + @Test + void testNewBundleWithClassFormat() throws IllegalAccessException, InstantiationException, IOException { + + ResourceBundle.Control control = new ResourceBundleUtf8Control(); + ClassLoader loader = getClass().getClassLoader(); + String baseName = "dummyClassBundle"; + + ResourceBundle bundle = control.newBundle(baseName, Locale.ENGLISH, "java.class", loader, false); + //because not have an actual class, bundle should be null + assertNull(bundle); + } + + @Test + void testReloading() throws IllegalAccessException, InstantiationException, IOException { + ResourceBundle.Control control = new ResourceBundleUtf8Control(); + ClassLoader loader = getClass().getClassLoader(); + String baseName = "msg"; + + // Test with reload flag + ResourceBundle bundle = control.newBundle(baseName, Locale.ENGLISH, "java.properties", loader, true); + assertNotNull(bundle); + assertEquals("Hello, World!", bundle.getString("hello")); + } + +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/SpringContextHolderTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/SpringContextHolderTest.java new file mode 100644 index 0000000..d08213e --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/SpringContextHolderTest.java @@ -0,0 +1,108 @@ +/* + * 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.support; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * Test case for {@link SpringContextHolder} + */ +class SpringContextHolderTest { + + private ApplicationContext applicationContext; + + private ConfigurableApplicationContext configurableApplicationContext; + + private SpringContextHolder springContextHolder; + + @BeforeEach + public void setUp() { + + applicationContext = mock(ApplicationContext.class); + configurableApplicationContext = mock(ConfigurableApplicationContext.class); + springContextHolder = new SpringContextHolder(); + } + + @Test + public void testSetApplicationContext() throws BeansException { + + springContextHolder.setApplicationContext(configurableApplicationContext); + + assertNotNull(SpringContextHolder.getApplicationContext()); + } + + @Test + public void testGetApplicationContext() { + + springContextHolder.setApplicationContext(applicationContext); + assertNotNull(SpringContextHolder.getApplicationContext()); + } + + @Test + public void testGetBeanByClass() { + + Class<String> beanClass = String.class; + String bean = "bean"; + when(applicationContext.getBean(beanClass)).thenReturn(bean); + + springContextHolder.setApplicationContext(applicationContext); + String retrievedBean = SpringContextHolder.getBean(beanClass); + + assertEquals(bean, retrievedBean); + } + + @Test + public void testShutdown() { + + springContextHolder.setApplicationContext(configurableApplicationContext); + SpringContextHolder.shutdown(); + + verify(configurableApplicationContext, times(1)).close(); + } + + @Test + public void testIsActive() { + + when(configurableApplicationContext.isActive()).thenReturn(true); + springContextHolder.setApplicationContext(configurableApplicationContext); + assertTrue(SpringContextHolder.isActive()); + } + + @Test + public void testAssertApplicationContextThrowsException() { + + RuntimeException exception = assertThrows(RuntimeException.class, SpringContextHolder::getApplicationContext); + assertEquals( + "applicationContext is null, please inject the springContextHolder", + exception.getMessage() + ); + } + +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/vaild/EmailParamValidatorTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/vaild/EmailParamValidatorTest.java new file mode 100644 index 0000000..51f19aa --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/vaild/EmailParamValidatorTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.support.vaild; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import jakarta.validation.ConstraintValidatorContext; +import org.apache.hertzbeat.common.support.valid.EmailParamValidator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test case for {@link EmailParamValidator} + */ + +class EmailParamValidatorTest { + + @InjectMocks + private EmailParamValidator emailParamValidator; + + @Mock + private ConstraintValidatorContext context; + + @Mock + private ConstraintValidatorContext.ConstraintViolationBuilder constraintViolationBuilder; + + @BeforeEach + public void setUp() { + MockitoAnnotations.initMocks(this); + when(context.buildConstraintViolationWithTemplate(any())).thenReturn(constraintViolationBuilder); + } + + @Test + public void testIsValid() { + boolean result = emailParamValidator.isValid(null, context); + assertFalse(result); + + result = emailParamValidator.isValid("123456345@qq.com", context); + assertTrue(result); + + result = emailParamValidator.isValid("", context); + assertFalse(result); + + result = emailParamValidator.isValid(" ", context); + assertFalse(result); + + result = emailParamValidator.isValid("test@example.com", context); + assertTrue(result); + + result = emailParamValidator.isValid("test@example", context); + assertFalse(result); + + result = emailParamValidator.isValid("test@sub.example.com", context); + assertTrue(result); + + + String longEmail = "verylongemailaddress@subdomain.domain.example.com"; + result = emailParamValidator.isValid(longEmail, context); + assertTrue(result); + } + +} + diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/vaild/HostParamValidatorTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/vaild/HostParamValidatorTest.java new file mode 100644 index 0000000..b60f77a --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/vaild/HostParamValidatorTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.support.vaild; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import jakarta.validation.ConstraintValidatorContext; +import org.apache.hertzbeat.common.support.valid.HostParamValidator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test case for {@link HostParamValidator} + */ + +class HostParamValidatorTest { + + @InjectMocks + private HostParamValidator hostParamValidator; + + @Mock + private ConstraintValidatorContext context; + + @Mock + private ConstraintValidatorContext.ConstraintViolationBuilder constraintViolationBuilder; + + @BeforeEach + public void setUp() { + MockitoAnnotations.initMocks(this); + when(context.buildConstraintViolationWithTemplate(any())).thenReturn(constraintViolationBuilder); + } + + @Test + public void testIsValid() { + boolean result = hostParamValidator.isValid(null, context); + assertTrue(result); + + result = hostParamValidator.isValid("", context); + assertTrue(result); + + result = hostParamValidator.isValid(" ", context); + assertTrue(result); + + result = hostParamValidator.isValid("192.168.1.1", context); + assertTrue(result); + + result = hostParamValidator.isValid("2001:0db8:85a3:0000:0000:8a2e:0370:7334", context); + assertTrue(result); + + result = hostParamValidator.isValid("www.example.com", context); + assertTrue(result); + + result = hostParamValidator.isValid("http://www.example.com", context); + assertTrue(result); + + result = hostParamValidator.isValid("https://www.baidu.com", context); + assertTrue(result); + + result = hostParamValidator.isValid("ht!tp://www.example.com", context); + assertFalse(result); + } +} + diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/vaild/PhoneNumParamValidatorTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/vaild/PhoneNumParamValidatorTest.java new file mode 100644 index 0000000..13e21a0 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/vaild/PhoneNumParamValidatorTest.java @@ -0,0 +1,74 @@ +/* + * 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.support.vaild; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import jakarta.validation.ConstraintValidatorContext; +import org.apache.hertzbeat.common.support.valid.PhoneNumParamValidator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test case for {@link PhoneNumParamValidator} + */ + +class PhoneNumParamValidatorTest { + + @InjectMocks + private PhoneNumParamValidator phoneNumParamValidator; + + @Mock + private ConstraintValidatorContext context; + + @Mock + private ConstraintValidatorContext.ConstraintViolationBuilder constraintViolationBuilder; + + @BeforeEach + public void setUp() { + MockitoAnnotations.initMocks(this); + when(context.buildConstraintViolationWithTemplate(any())).thenReturn(constraintViolationBuilder); + } + + @Test + public void testIsValid() { + boolean result = phoneNumParamValidator.isValid(null, context); + assertFalse(result); + + result = phoneNumParamValidator.isValid("", context); + assertFalse(result); + + result = phoneNumParamValidator.isValid("123456", context); + assertFalse(result); + + result = phoneNumParamValidator.isValid("abc123", context); + assertFalse(result); + + result = phoneNumParamValidator.isValid("13900001234", context); + assertTrue(result); + + result = phoneNumParamValidator.isValid("1234567890123456789", context); + assertFalse(result); + } +} + diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/valid/SqlSecurityValidatorTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/valid/SqlSecurityValidatorTest.java new file mode 100644 index 0000000..782ee31 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/valid/SqlSecurityValidatorTest.java @@ -0,0 +1,267 @@ +/* + * 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.support.valid; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test for {@link SqlSecurityValidator} + */ +class SqlSecurityValidatorTest { + + private SqlSecurityValidator validator; + + @BeforeEach + void setUp() { + validator = new SqlSecurityValidator(Arrays.asList("hertzbeat_logs", "app_logs", "access_logs")); + } + + @Test + void testValidSelectStatement() { + assertDoesNotThrow(() -> validator.validate("SELECT * FROM hertzbeat_logs")); + assertDoesNotThrow(() -> validator.validate("SELECT id, message FROM hertzbeat_logs WHERE level = 'ERROR'")); + assertDoesNotThrow(() -> validator.validate("SELECT COUNT(*) FROM app_logs")); + assertDoesNotThrow(() -> validator.validate("select * from HERTZBEAT_LOGS")); // case insensitive + } + + @Test + void testSelectWithJoin() { + assertDoesNotThrow(() -> validator.validate( + "SELECT a.id, b.message FROM hertzbeat_logs a JOIN app_logs b ON a.id = b.id")); + } + + @Test + void testSelectWithSubqueryNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("SELECT * FROM hertzbeat_logs WHERE id IN (SELECT id FROM app_logs)")); + } + + @Test + void testSelectWithSubqueryInFromNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("SELECT * FROM (SELECT * FROM hertzbeat_logs) AS subq")); + } + + @Test + void testEmptySql() { + assertThrows(SqlSecurityException.class, () -> validator.validate(null)); + assertThrows(SqlSecurityException.class, () -> validator.validate("")); + assertThrows(SqlSecurityException.class, () -> validator.validate(" ")); + } + + @Test + void testInvalidSqlSyntax() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("SELECT * FORM hertzbeat_logs")); // typo: FORM instead of FROM + } + + @Test + void testInsertNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("INSERT INTO hertzbeat_logs (message) VALUES ('test')")); + } + + @Test + void testUpdateNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("UPDATE hertzbeat_logs SET message = 'test' WHERE id = 1")); + } + + @Test + void testDeleteNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("DELETE FROM hertzbeat_logs WHERE id = 1")); + } + + @Test + void testDropNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("DROP TABLE hertzbeat_logs")); + } + + @Test + void testTruncateNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("TRUNCATE TABLE hertzbeat_logs")); + } + + @Test + void testAlterNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("ALTER TABLE hertzbeat_logs ADD COLUMN new_col VARCHAR(100)")); + } + + @Test + void testCreateNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("CREATE TABLE new_table (id INT)")); + } + + @Test + void testUnauthorizedTable() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("SELECT * FROM users")); + } + + @Test + void testUnauthorizedTableInJoin() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("SELECT * FROM hertzbeat_logs JOIN users ON hertzbeat_logs.user_id = users.id")); + } + + @Test + void testUnauthorizedTableInSubquery() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("SELECT * FROM hertzbeat_logs WHERE user_id IN (SELECT id FROM users)")); + } + + @Test + void testTableWithQuotes() { + assertDoesNotThrow(() -> validator.validate("SELECT * FROM \"hertzbeat_logs\"")); + assertDoesNotThrow(() -> validator.validate("SELECT * FROM `hertzbeat_logs`")); + } + + @Test + void testEmptyAllowedTables() { + SqlSecurityValidator emptyValidator = new SqlSecurityValidator(Collections.emptyList()); + assertThrows(SqlSecurityException.class, + () -> emptyValidator.validate("SELECT * FROM any_table")); + } + + @Test + void testNullAllowedTables() { + SqlSecurityValidator nullValidator = new SqlSecurityValidator(null); + assertThrows(SqlSecurityException.class, + () -> nullValidator.validate("SELECT * FROM any_table")); + } + + @Test + void testComplexSelectWithAggregation() { + assertDoesNotThrow(() -> validator.validate( + "SELECT level, COUNT(*) as cnt FROM hertzbeat_logs GROUP BY level HAVING COUNT(*) > 10 ORDER BY cnt DESC LIMIT 100")); + } + + @Test + void testSelectWithUnionNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("SELECT * FROM hertzbeat_logs UNION SELECT * FROM app_logs")); + } + + @Test + void testSelectWithUnionAllNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("SELECT * FROM hertzbeat_logs UNION ALL SELECT * FROM app_logs")); + } + + @Test + void testSelectWithIntersectNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("SELECT * FROM hertzbeat_logs INTERSECT SELECT * FROM app_logs")); + } + + @Test + void testSelectWithExceptNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("SELECT * FROM hertzbeat_logs EXCEPT SELECT * FROM app_logs")); + } + + @Test + void testLateralSubqueryNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("SELECT * FROM hertzbeat_logs, LATERAL (SELECT * FROM app_logs) AS t")); + } + + @Test + void testWithClauseNotAllowed() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("WITH cte AS (SELECT * FROM hertzbeat_logs) SELECT * FROM cte")); + } + + @Test + void testSqlInjectionAttemptDropTable() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("DROP TABLE users")); + } + + @Test + void testSqlInjectionAttemptUnauthorizedTable() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("SELECT * FROM users")); + } + + @Test + void testSqlInjectionAttemptDeleteFrom() { + assertThrows(SqlSecurityException.class, + () -> validator.validate("DELETE FROM hertzbeat_logs WHERE 1=1")); + } + + @Test + void testBypassInSelectItems() { + assertThrows(SqlSecurityException.class, () -> validator.validate( + "SELECT (SELECT password FROM secret_table) FROM hertzbeat_logs")); + } + + @Test + void testBypassInWhereClauseAnd() { + assertThrows(SqlSecurityException.class, () -> validator.validate( + "SELECT * FROM hertzbeat_logs WHERE 1=1 AND id IN (SELECT id FROM secret_table)")); + } + + @Test + void testBypassInFunction() { + assertThrows(SqlSecurityException.class, () -> validator.validate( + "SELECT * FROM hertzbeat_logs WHERE id = abs((SELECT count(*) FROM secret_table))")); + } + + @Test + void testBypassInCaseWhen() { + assertThrows(SqlSecurityException.class, () -> validator.validate( + "SELECT * FROM hertzbeat_logs WHERE status = (CASE WHEN (SELECT 1 FROM secret_table)=1 THEN 1 ELSE 0 END)")); + } + + @Test + void testBypassWithAndExpression() { + assertThrows(SqlSecurityException.class, () -> validator.validate( + "SELECT * FROM hertzbeat_logs WHERE 1=1 AND id = (SELECT id FROM secret_table)")); + } + + @Test + void testBypassWithGreaterThan() { + assertThrows(SqlSecurityException.class, () -> validator.validate( + "SELECT * FROM hertzbeat_logs WHERE id > (SELECT count(*) FROM secret_table)")); + } + + @Test + void testBypassWithBetween() { + assertThrows(SqlSecurityException.class, () -> validator.validate( + "SELECT * FROM hertzbeat_logs WHERE id BETWEEN 1 AND (SELECT id FROM secret_table)")); + } + + @Test + void testBypassWithMathOperations() { + assertThrows(SqlSecurityException.class, () -> validator.validate( + "SELECT * FROM hertzbeat_logs WHERE id = 1 + (SELECT id FROM secret_table)")); + } +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/util/FileUtilTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/util/FileUtilTest.java new file mode 100644 index 0000000..d19d7c2 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/util/FileUtilTest.java @@ -0,0 +1,73 @@ +/* + * 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.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.apache.hertzbeat.common.constants.ExportFileConstants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +import java.nio.charset.StandardCharsets; + +/** + * test case for {@link FileUtil}. + */ + +class FileUtilTest { + + private static final String EXCEL_TYPE = "application/vnd.ms-excel"; + private static final String YAML_TYPE = "application/x-yaml"; + + private MockMultipartFile jsonFile; + private MockMultipartFile excelFile; + private MockMultipartFile yamlFile; + private MockMultipartFile emptyFile; + + @BeforeEach + void setUp() { + + jsonFile = new MockMultipartFile("file", "test.json", MediaType.APPLICATION_JSON_VALUE, + "test content".getBytes(StandardCharsets.UTF_8)); + excelFile = new MockMultipartFile("file", "test.xlsx", EXCEL_TYPE, + "test content".getBytes(StandardCharsets.UTF_8)); + yamlFile = new MockMultipartFile("file", "test.yaml", YAML_TYPE, + "test content".getBytes(StandardCharsets.UTF_8)); + emptyFile = new MockMultipartFile("file", "", null, (byte[]) null); + } + + @Test + void testGetFileName() { + + assertEquals("test.json", FileUtil.getFileName(jsonFile)); + assertEquals("test.xlsx", FileUtil.getFileName(excelFile)); + assertEquals("test.yaml", FileUtil.getFileName(yamlFile)); + assertEquals("", FileUtil.getFileName(emptyFile)); + } + + @Test + void testGetFileType() { + + assertEquals(ExportFileConstants.JsonFile.TYPE, FileUtil.getFileType(jsonFile)); + assertEquals(ExportFileConstants.ExcelFile.TYPE, FileUtil.getFileType(excelFile)); + assertEquals(ExportFileConstants.YamlFile.TYPE, FileUtil.getFileType(yamlFile)); + assertEquals("", FileUtil.getFileType(emptyFile)); + } + +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/util/ResourceBundleUtilTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/util/ResourceBundleUtilTest.java new file mode 100644 index 0000000..6ac67d4 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/util/ResourceBundleUtilTest.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.common.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mockStatic; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link ResourceBundleUtil} + */ +@ExtendWith(MockitoExtension.class) +class ResourceBundleUtilTest { + + private static final String BUNDLE_NAME = "TestBundle"; + + @BeforeEach + void setUp() { + Locale.setDefault(Locale.US); + } + + @Test + void testGetBundleWithValidBundleName() { + try (MockedStatic<ResourceBundle> mockedResourceBundle = mockStatic(ResourceBundle.class)) { + ResourceBundle mockBundle = Mockito.mock(ResourceBundle.class); + + mockedResourceBundle.when( + () -> ResourceBundle.getBundle( + Mockito.eq(BUNDLE_NAME), + Mockito.any(ResourceBundle.Control.class) + ) + ).thenReturn(mockBundle); + + ResourceBundle bundle = ResourceBundleUtil.getBundle(BUNDLE_NAME); + + assertNotNull(bundle); + assertEquals(mockBundle, bundle); + } + } + + @Test + void testGetBundleByInvalidBundleName() { + try (MockedStatic<ResourceBundle> mockedResourceBundle = mockStatic(ResourceBundle.class)) { + mockedResourceBundle.when( + () -> ResourceBundle.getBundle( + Mockito.eq(BUNDLE_NAME), + Mockito.any(ResourceBundle.Control.class) + ) + ).thenThrow(new MissingResourceException("Missing bundle", "ResourceBundle", BUNDLE_NAME)); + + ResourceBundle mockDefaultBundle = Mockito.mock(ResourceBundle.class); + + mockedResourceBundle.when(() -> ResourceBundle.getBundle( + Mockito.eq(BUNDLE_NAME), + Mockito.eq(Locale.US), + Mockito.any(ResourceBundle.Control.class)) + ).thenReturn(mockDefaultBundle); + + ResourceBundle bundle = ResourceBundleUtil.getBundle(BUNDLE_NAME); + + assertNotNull(bundle); + assertEquals(mockDefaultBundle, bundle); + } + } + +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/util/ResponseUtilTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/util/ResponseUtilTest.java new file mode 100644 index 0000000..b81a935 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/util/ResponseUtilTest.java @@ -0,0 +1,86 @@ +/* + * 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.util; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.naming.AuthenticationException; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.junit.jupiter.api.Test; +import org.springframework.http.ResponseEntity; + +/** + * Test for {@link ResponseUtil} + */ +public class ResponseUtilTest { + @Test + public void testHandle() { + assertDoesNotThrow(() -> { + ResponseUtil.handle(() -> "test"); + }); + + assertDoesNotThrow(() -> { + ResponseEntity<Message<String>> resp = ResponseUtil.handle(() -> { + throw new RuntimeException("test"); + }); + assertEquals(CommonConstants.FAIL_CODE, resp.getBody().getCode()); + }); + + assertDoesNotThrow(() -> { + ResponseEntity<Message<String>> resp = ResponseUtil.handle(() -> { + throw new AuthenticationException("test"); + }); + assertEquals(CommonConstants.LOGIN_FAILED_CODE, resp.getBody().getCode()); + }); + + assertDoesNotThrow(() -> { + ResponseUtil.Runnable run = new ResponseUtil.Runnable() { + public void run() { + throw new UnsupportedOperationException("Unimplemented method 'run'"); + } + }; + ResponseEntity<Message<String>> resp = ResponseUtil.handle(run); + assertEquals(CommonConstants.FAIL_CODE, resp.getBody().getCode()); + }); + + assertDoesNotThrow(() -> { + ResponseUtil.Runnable run = new ResponseUtil.Runnable() { + public void run() throws AuthenticationException { + throw new AuthenticationException("test"); + } + }; + ResponseEntity<Message<String>> resp = ResponseUtil.handle(run); + assertEquals(CommonConstants.LOGIN_FAILED_CODE, resp.getBody().getCode()); + }); + + assertDoesNotThrow(() -> { + ResponseEntity<Message<String>> resp = ResponseUtil.handle(() -> {}); + assertEquals(CommonConstants.SUCCESS_CODE, resp.getBody().getCode()); + }); + } + + /** + * InnerResponseUtilTest + */ + public interface InnerResponseUtilTest { + void run() throws Exception; + } +} diff --git a/hertzbeat-common-spring/src/test/resources/msg.properties b/hertzbeat-common-spring/src/test/resources/msg.properties new file mode 100644 index 0000000..6dc68e4 --- /dev/null +++ b/hertzbeat-common-spring/src/test/resources/msg.properties @@ -0,0 +1,16 @@ +# 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. + +hello=你好 diff --git a/hertzbeat-common-spring/src/test/resources/msg_en.properties b/hertzbeat-common-spring/src/test/resources/msg_en.properties new file mode 100644 index 0000000..7514bf5 --- /dev/null +++ b/hertzbeat-common-spring/src/test/resources/msg_en.properties @@ -0,0 +1,16 @@ +# 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. + +hello=Hello, World! diff --git a/hertzbeat-e2e/hertzbeat-collector-basic-e2e/pom.xml b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/pom.xml new file mode 100644 index 0000000..3166613 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/pom.xml @@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-e2e</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-collector-basic-e2e</artifactId> + + <properties> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <postgresql.version>42.5.5</postgresql.version> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-common-e2e</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + <type>test-jar</type> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-basic</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-common</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + + <!-- Testcontainers Jupiter / JUnit 5 --> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers-junit-jupiter</artifactId> + <scope>test</scope> + </dependency> + <!-- Testcontainers JDBC Support --> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers-jdbc</artifactId> + <scope>test</scope> + </dependency> + <!-- Testcontainers Database Modules --> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers-mysql</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers-postgresql</artifactId> + <scope>test</scope> + </dependency> + + <!-- JDBC Drivers --> + <dependency> + <groupId>com.mysql</groupId> + <artifactId>mysql-connector-j</artifactId> + <scope>test</scope> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.postgresql</groupId> + <artifactId>postgresql</artifactId> + <version>${postgresql.version}</version> + <scope>test</scope> + <optional>true</optional> + </dependency> + + </dependencies> +</project> diff --git a/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/database/DatabaseImagesEnum.java b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/database/DatabaseImagesEnum.java new file mode 100644 index 0000000..2d7fc2e --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/database/DatabaseImagesEnum.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.basic.database; + +/** + * Database type (image mame) and image tag enumeration class + */ +public enum DatabaseImagesEnum { + MYSQL("mysql", "8.0.36"), + POSTGRESQL("postgresql", "15"); + + private final String imageName; + private final String defaultTag; + + DatabaseImagesEnum(String imageName, String defaultTag) { + this.imageName = imageName; + this.defaultTag = defaultTag; + } + + public String getImageName() { + return imageName; + } + + public String getDefaultTag() { + return defaultTag; + } + + public String getFullImageName() { + return imageName + ":" + defaultTag; + } + + public static DatabaseImagesEnum fromImageName(String imageName) { + for (DatabaseImagesEnum value : values()) { + if (value.getImageName().equalsIgnoreCase(imageName)) { + return value; + } + } + throw new IllegalArgumentException("Unknown database image name: " + imageName); + } + + public static DatabaseImagesEnum fromFullImageName(String fullImageName) { + for (DatabaseImagesEnum value : values()) { + if (value.getFullImageName().equalsIgnoreCase(fullImageName)) { + return value; + } + } + throw new IllegalArgumentException("Unknown full database image name: " + fullImageName); + } + + @Override + public String toString() { + return "DatabaseImagesEnum{" + + "imageName='" + imageName + '\'' + + ", defaultTag='" + defaultTag + '\'' + + '}'; + } +} diff --git a/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/database/JdbcCommonCollectE2eTest.java b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/database/JdbcCommonCollectE2eTest.java new file mode 100644 index 0000000..69f8492 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/database/JdbcCommonCollectE2eTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.basic.database; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollectE2eTest; +import org.apache.hertzbeat.collector.collect.database.JdbcCommonCollect; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.Protocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +/** + * E2e test monitored by Jdbc Common + */ +@Slf4j +@ExtendWith(MockitoExtension.class) +public class JdbcCommonCollectE2eTest extends AbstractCollectE2eTest { + private static final DatabaseImagesEnum databaseImage = DatabaseImagesEnum.fromImageName("postgresql"); + + private static final String DATABASE_IMAGE_NAME = databaseImage.getImageName(); + + private static final String DATABASE_IMAGE_TAG = databaseImage.getDefaultTag(); + + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + collect = new JdbcCommonCollect(); + metrics = new Metrics(); + } + + @Override + protected CollectRep.MetricsData.Builder collectMetrics(Metrics metricsDef) { + JdbcProtocol jdbcProtocol = (JdbcProtocol) buildProtocol(metricsDef); + metrics.setJdbc(jdbcProtocol); + CollectRep.MetricsData.Builder metricsData = CollectRep.MetricsData.newBuilder(); + metricsData.setApp(DATABASE_IMAGE_NAME); + metrics.setAliasFields(metricsDef.getAliasFields()); + return collectMetricsData(metrics, metricsDef, metricsData); + } + + @Override + protected Protocol buildProtocol(Metrics metricsDef) { + JdbcProtocol jdbcProtocol = metricsDef.getJdbc(); + jdbcProtocol.setHost(DATABASE_IMAGE_NAME); + jdbcProtocol.setPort(DATABASE_IMAGE_TAG); + jdbcProtocol.setDatabase("test"); + jdbcProtocol.setPlatform("testcontainers"); + return jdbcProtocol; + } + + @Test + public void testWithJdbcTcUrl() { + Job dockerJob = appService.getAppDefine(DATABASE_IMAGE_NAME); + List<Map<String, Configmap>> configmapFromPreCollectData = new LinkedList<>(); + for (Metrics metricsDef : dockerJob.getMetrics()) { + metricsDef = CollectUtil.replaceCryPlaceholderToMetrics(metricsDef, configmapFromPreCollectData.size() > 0 ? configmapFromPreCollectData.get(0) : new HashMap<>()); + String metricName = metricsDef.getName(); + if ("slow_sql".equals(metricName)) { + Metrics finalMetricsDef = metricsDef; + assertDoesNotThrow(() -> collectMetrics(finalMetricsDef), + String.format("%s failed to collect metrics)", metricName)); + log.info("{} metrics validation passed", metricName); + continue; // skip slow_sql, extra extensions + } + CollectRep.MetricsData metricsData = validateMetricsCollection(metricsDef, metricName, true); + CollectUtil.getConfigmapFromPreCollectData(metricsData); + } + } +} diff --git a/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/http/DockerMonitorE2eTest.java b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/http/DockerMonitorE2eTest.java new file mode 100644 index 0000000..ca74b7b --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/http/DockerMonitorE2eTest.java @@ -0,0 +1,144 @@ +/* + * 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.collector.collect.basic.http; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollectE2eTest; +import org.apache.hertzbeat.collector.collect.http.HttpCollectImpl; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.Protocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.util.ResourceUtils; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Integration test for Docker monitoring functionality + */ +@Slf4j +@ExtendWith(MockitoExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class DockerMonitorE2eTest extends AbstractCollectE2eTest { + + private static final int MOCK_SERVER_PORT = 52375; + private static final String LOCALHOST = "127.0.0.1"; + private static HttpServer mockServer; + + @AfterAll + public static void tearDown() { + if (mockServer != null) { + mockServer.stop(0); + } + } + + @BeforeEach + public void setUp() throws Exception { + + super.setUp(); + // Setup collect instance + collect = new HttpCollectImpl(); + + // Setup mock server and endpoints + mockServer = HttpServer.create(new InetSocketAddress(MOCK_SERVER_PORT), 0); + mockServer.setExecutor(null); + mockServer.start(); + + // Setup Docker API endpoints + String containerResponse = loadResponseFromFile("classpath:http/docker/containers_result.txt"); + String infoResponse = loadResponseFromFile("classpath:http/docker/system_result.txt"); + String containerStatsResponse = loadResponseFromFile("classpath:http/docker/containers_stats.txt"); + + mockServer.createContext("/containers/json", exchange -> { + String query = exchange.getRequestURI().getQuery(); + if (query != null && query.contains("all=true")) { + sendJsonResponse(exchange, containerResponse); + } else { + exchange.sendResponseHeaders(404, 0); + exchange.close(); + } + }); + + mockServer.createContext("/info", exchange -> sendJsonResponse(exchange, infoResponse)); + mockServer.createContext("/containers/34174a918eb2e38cdb097c910f74af845e7383b04765d26ad52f940f86342a64/stats", exchange -> sendJsonResponse(exchange, containerStatsResponse)); + + } + + private String loadResponseFromFile(String resourcePath) throws Exception { + return new String(Files.readAllBytes(ResourceUtils.getFile(resourcePath).toPath())); + } + + private void sendJsonResponse(HttpExchange exchange, String response) throws IOException { + exchange.getResponseHeaders().set("Content-Type", "application/json"); + final byte[] array = response.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, array.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(array); + } + } + + @Test + public void testDockerMonitor() { + Job dockerJob = appService.getAppDefine("docker"); + List<Map<String, Configmap>> configmapFromPreCollectData = new LinkedList<>(); + for (Metrics metricsDef : dockerJob.getMetrics()) { + metricsDef = CollectUtil.replaceCryPlaceholderToMetrics(metricsDef, configmapFromPreCollectData.size() > 0 ? configmapFromPreCollectData.get(0) : new HashMap<>()); + CollectRep.MetricsData metricsData = validateMetricsCollection(metricsDef, metricsDef.getName()); + configmapFromPreCollectData = CollectUtil.getConfigmapFromPreCollectData(metricsData); + } + } + + @Override + protected Protocol buildProtocol(Metrics metricsDef) { + // Setup HTTP protocol + HttpProtocol protocol = new HttpProtocol(); + protocol.setHost(LOCALHOST); + protocol.setPort(String.valueOf(MOCK_SERVER_PORT)); + protocol.setMethod(metricsDef.getHttp().getMethod()); + protocol.setParseType(metricsDef.getHttp().getParseType()); + protocol.setParseScript(metricsDef.getHttp().getParseScript()); + protocol.setUrl(metricsDef.getHttp().getUrl()); + return protocol; + } + + @Override + protected CollectRep.MetricsData.Builder collectMetrics(Metrics metricsDef) { + HttpProtocol protocol = (HttpProtocol) buildProtocol(metricsDef); + metrics.setHttp(protocol); + return collectMetricsData(metrics, metricsDef); + } +} \ No newline at end of file diff --git a/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/http/HttpMonitorE2eTest.java b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/http/HttpMonitorE2eTest.java new file mode 100644 index 0000000..a6f5b15 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/http/HttpMonitorE2eTest.java @@ -0,0 +1,129 @@ +/* + * 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.collector.collect.basic.http; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollectE2eTest; +import org.apache.hertzbeat.collector.collect.http.HttpCollectImpl; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.Protocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * E2E test for HTTP monitor + */ +@Slf4j +@ExtendWith(MockitoExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class HttpMonitorE2eTest extends AbstractCollectE2eTest { + + private static final int MOCK_SERVER_PORT = 52376; + private static final String LOCALHOST = "127.0.0.1"; + private static final String RELATIVE_PATH = "/"; + private static final List<String> ALLOW_EMPTY_WHITE_LIST = List.of("header"); + private static HttpServer mockServer; + + @AfterAll + public static void tearDown() { + if (mockServer != null) { + mockServer.stop(0); + } + } + + @BeforeEach + public void setUp() throws Exception { + + super.setUp(); + // Setup collect instance + collect = new HttpCollectImpl(); + + // Setup mock server and endpoints + mockServer = HttpServer.create(new InetSocketAddress(MOCK_SERVER_PORT), 0); + mockServer.setExecutor(null); + mockServer.start(); + + mockServer.createContext(RELATIVE_PATH, exchange -> sendJsonResponse(exchange, "")); + + } + + private void sendJsonResponse(HttpExchange exchange, String response) throws IOException { + exchange.getResponseHeaders().set("Content-Type", "application/json"); + final byte[] array = response.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, array.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(array); + } + } + + @Test + public void testHttpMonitor() { + Job dockerJob = appService.getAppDefine("api"); + List<Map<String, Configmap>> configmapFromPreCollectData = new LinkedList<>(); + for (Metrics metricsDef : dockerJob.getMetrics()) { + metricsDef = CollectUtil.replaceCryPlaceholderToMetrics(metricsDef, configmapFromPreCollectData.size() > 0 ? configmapFromPreCollectData.get(0) : new HashMap<>()); + CollectRep.MetricsData metricsData; + if (ALLOW_EMPTY_WHITE_LIST.contains(metricsDef.getName())) { + metricsData = validateMetricsCollection(metricsDef, metricsDef.getName(), true); + } else { + metricsData = validateMetricsCollection(metricsDef, metricsDef.getName()); + } + configmapFromPreCollectData = CollectUtil.getConfigmapFromPreCollectData(metricsData); + } + } + + @Override + protected Protocol buildProtocol(Metrics metricsDef) { + // Setup HTTP protocol + HttpProtocol protocol = new HttpProtocol(); + protocol.setHost(LOCALHOST); + protocol.setUrl(RELATIVE_PATH); + protocol.setMethod("GET"); + protocol.setPort(String.valueOf(MOCK_SERVER_PORT)); + protocol.setParseType(metricsDef.getHttp().getParseType()); + protocol.setParseScript(metricsDef.getHttp().getParseScript()); + return protocol; + } + + @Override + protected CollectRep.MetricsData.Builder collectMetrics(Metrics metricsDef) { + HttpProtocol protocol = (HttpProtocol) buildProtocol(metricsDef); + metrics.setHttp(protocol); + return collectMetricsData(metrics, metricsDef); + } +} \ No newline at end of file diff --git a/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/redis/RedisCommonCollectE2eTest.java b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/redis/RedisCommonCollectE2eTest.java new file mode 100644 index 0000000..673cbb0 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/redis/RedisCommonCollectE2eTest.java @@ -0,0 +1,126 @@ +/* + * 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.collector.collect.basic.redis; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollectE2eTest; +import org.apache.hertzbeat.collector.collect.redis.RedisCommonCollectImpl; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.Protocol; +import org.apache.hertzbeat.common.entity.job.protocol.RedisProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.lifecycle.Startables; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; + +/** + * End-to-end test class for redis collector + */ +@Slf4j +@ExtendWith(MockitoExtension.class) +public class RedisCommonCollectE2eTest extends AbstractCollectE2eTest { + private static final String REDIS_IMAGE = "redis:7.4.2"; + private static final String HOST = "127.0.0.1"; + private static final int REDIS_PORT = 6379; + private static final String REDIS_PATTERN = "1"; + private static final List<String> ALLOW_EMPTY_WHITE_LIST = Arrays.asList("server", "errorstats", "commandstats", "keyspace"); + + private static GenericContainer<?> redisContainer; + + private Integer mappedPort; + + @AfterAll + public static void tearDown() throws InterruptedException { + if (redisContainer != null) { + redisContainer.stop(); + } + } + + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + collect = new RedisCommonCollectImpl(); + + // Set up and start container + setupAndStartContainer(); + + mappedPort = redisContainer.getMappedPort(REDIS_PORT); + log.info("Container started successfully with mapped port: {}", mappedPort); + } + + @Test + public void testRedisCollect() throws ExecutionException, InterruptedException, TimeoutException { + Assertions.assertTrue(redisContainer.isRunning(), "redis container should be running"); + + Job redisJob = appService.getAppDefine("redis"); + redisJob.getMetrics().forEach(metricsDef -> { + if (ALLOW_EMPTY_WHITE_LIST.contains(metricsDef.getName())) { + validateMetricsCollection(metricsDef, metricsDef.getName(), true); + } else { + validateMetricsCollection(metricsDef, metricsDef.getName()); + } + }); + } + + @Override + protected CollectRep.MetricsData.Builder collectMetrics(Metrics metricsDef) { + //Build redis protocol configuration + RedisProtocol redisProtocol = (RedisProtocol) buildProtocol(metricsDef); + metrics.setRedis(redisProtocol); + metrics.setName(metricsDef.getName()); + metrics.setFields(metricsDef.getFields()); + return collectMetricsData(metrics, metricsDef); + } + + @Override + protected Protocol buildProtocol(Metrics metricsDef) { + RedisProtocol redisProtocol = new RedisProtocol(); + redisProtocol.setHost(HOST); + redisProtocol.setPort(mappedPort.toString()); + redisProtocol.setPattern(REDIS_PATTERN); + return redisProtocol; + } + + public void setupAndStartContainer() { + Network network = Network.builder().build(); + redisContainer = new GenericContainer<>(DockerImageName.parse(REDIS_IMAGE)) + .withExposedPorts(REDIS_PORT) + .withNetwork(network) + .withNetworkAliases("redis") + .waitingFor(Wait.forListeningPort()) + .withStartupTimeout(Duration.ofSeconds(30)); + + Startables.deepStart(Stream.of(redisContainer)).join(); + } +} diff --git a/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/ssh/SshCollectE2eTest.java b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/ssh/SshCollectE2eTest.java new file mode 100644 index 0000000..95e8a1b --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/ssh/SshCollectE2eTest.java @@ -0,0 +1,150 @@ +/* + * 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.collector.collect.basic.ssh; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollectE2eTest; +import org.apache.hertzbeat.collector.collect.ssh.SshCollectImpl; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.Protocol; +import org.apache.hertzbeat.common.entity.job.protocol.SshProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.lifecycle.Startables; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; + +/** + * End-to-end test class for SSH collector + */ +@Slf4j +@ExtendWith(MockitoExtension.class) +public class SshCollectE2eTest extends AbstractCollectE2eTest { + private static final String UBUNTU_IMAGE = "rastasheep/ubuntu-sshd:18.04"; + private static final String HOST = "127.0.0.1"; + private static final String ROOT_USER = "root"; + private static final int SSH_PORT = 22; + private static final int PASSWORD_LENGTH = 12; + private static final List<String> ALLOW_EMPTY_WHITE_LIST = Arrays.asList("top_mem_process", "top_cpu_process"); + + private static GenericContainer<?> linuxContainer; + + private Integer mappedPort; + private String password; + + @AfterAll + public static void tearDown() { + if (linuxContainer != null) { + linuxContainer.stop(); + } + } + + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + collect = new SshCollectImpl(); + password = generateRandomPassword(); + + // Set up and start container + setupAndStartContainer(); + + // Set root password + setContainerRootPassword(); + + mappedPort = linuxContainer.getMappedPort(SSH_PORT); + log.info("Container started successfully with mapped port: {}", mappedPort); + } + + @Test + public void testSshCollect() throws ExecutionException, InterruptedException, TimeoutException { + Assertions.assertTrue(linuxContainer.isRunning(), "Ubuntu container should be running"); + + Job ubuntuJob = appService.getAppDefine("ubuntu"); + ubuntuJob.getMetrics().forEach(metricsDef -> { + if (ALLOW_EMPTY_WHITE_LIST.contains(metricsDef.getName())) { + validateMetricsCollection(metricsDef, metricsDef.getName(), true); + } else { + validateMetricsCollection(metricsDef, metricsDef.getName()); + } + }); + } + + @Override + protected CollectRep.MetricsData.Builder collectMetrics(Metrics metricsDef) { + // Build SSH protocol configuration + SshProtocol sshProtocol = (SshProtocol) buildProtocol(metricsDef); + metrics.setSsh(sshProtocol); + return collectMetricsData(metrics, metricsDef); + } + + @Override + protected Protocol buildProtocol(Metrics metricsDef) { + SshProtocol sshProtocol = new SshProtocol(); + sshProtocol.setHost(HOST); + sshProtocol.setPort(mappedPort.toString()); + sshProtocol.setUsername(ROOT_USER); + sshProtocol.setPassword(password); + sshProtocol.setScript(metricsDef.getSsh().getScript()); + sshProtocol.setParseType(metricsDef.getSsh().getParseType()); + return sshProtocol; + } + + private void setupAndStartContainer() { + Network network = Network.builder().build(); + linuxContainer = new GenericContainer<>(DockerImageName.parse(UBUNTU_IMAGE)) + .withExposedPorts(SSH_PORT) + .withNetwork(network) + .withNetworkAliases("ubuntu") + .waitingFor(Wait.forListeningPort()) + .withCommand("/usr/sbin/sshd", "-D") + .withStartupTimeout(Duration.ofSeconds(30)); + + Startables.deepStart(Stream.of(linuxContainer)).join(); + } + + private void setContainerRootPassword() throws Exception { + linuxContainer.execInContainer("bash", "-c", + String.format("echo 'root:%s' | chpasswd", password)); + } + + private String generateRandomPassword() { + String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder password = new StringBuilder(); + Random random = new Random(); + for (int i = 0; i < PASSWORD_LENGTH; i++) { + password.append(characters.charAt(random.nextInt(characters.length()))); + } + return password.toString(); + } +} diff --git a/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/telnet/ZookeeperMonitorE2eTest.java b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/telnet/ZookeeperMonitorE2eTest.java new file mode 100644 index 0000000..e76fcf5 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/telnet/ZookeeperMonitorE2eTest.java @@ -0,0 +1,128 @@ +/* + * 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.collector.collect.basic.telnet; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollectE2eTest; +import org.apache.hertzbeat.collector.collect.telnet.TelnetCollectImpl; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.Protocol; +import org.apache.hertzbeat.common.entity.job.protocol.TelnetProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Integration test for Zookeeper monitoring functionality + */ +@Slf4j +@ExtendWith(MockitoExtension.class) +public class ZookeeperMonitorE2eTest extends AbstractCollectE2eTest { + + private static final String ZOOKEEPER_IMAGE_NAME = "zookeeper:3.8.4"; + private static final String ZOOKEEPER_NAME = "zookeeper"; + private static final Integer ZOOKEEPER_PORT = 2181; + private static GenericContainer<?> zookeeperContainer; + + @AfterAll + public static void tearDown() { + if (zookeeperContainer != null) { + zookeeperContainer.stop(); + } + } + + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + collect = new TelnetCollectImpl(); + metrics = new Metrics(); + + try { + // Start Zookeeper container with custom configuration + zookeeperContainer = new GenericContainer<>(DockerImageName.parse(ZOOKEEPER_IMAGE_NAME)) + .withExposedPorts(ZOOKEEPER_PORT) + .withEnv("ZOO_4LW_COMMANDS_WHITELIST", "*") + .withNetworkAliases(ZOOKEEPER_NAME) + .waitingFor( + Wait.forLogMessage(".*Started AdminServer on address.*\\n", 1) + .withStartupTimeout(Duration.ofSeconds(60)) + ) + .withLogConsumer(outputFrame -> { + log.info(outputFrame.getUtf8String()); + }); + + zookeeperContainer.start(); + log.info("Zookeeper container started at {}:{}", + zookeeperContainer.getHost(), + zookeeperContainer.getMappedPort(ZOOKEEPER_PORT)); + } catch (Exception e) { + e.printStackTrace(); + log.error("Failed to start Zookeeper container", e); + throw e; + } + Thread.sleep(30000); + } + + @Override + protected CollectRep.MetricsData.Builder collectMetrics(Metrics metricsDef) { + TelnetProtocol telnetProtocol = (TelnetProtocol) buildProtocol(metricsDef); + metrics.setTelnet(telnetProtocol); + CollectRep.MetricsData.Builder metricsData = CollectRep.MetricsData.newBuilder(); + metricsData.setApp(ZOOKEEPER_NAME); + metrics.setAliasFields(metricsDef.getAliasFields()); + return collectMetricsData(metrics, metricsDef, metricsData); + } + + @Override + protected Protocol buildProtocol(Metrics metricsDef) { + TelnetProtocol protocol = new TelnetProtocol(); + protocol.setHost(zookeeperContainer.getHost()); + protocol.setPort(String.valueOf(zookeeperContainer.getMappedPort(ZOOKEEPER_PORT))); + protocol.setCmd(metricsDef.getTelnet().getCmd()); + return protocol; + } + + @Test + public void testZookeeperMonitor() { + Assertions.assertTrue(zookeeperContainer.isRunning(), "Zookeeper container should be running"); + + Job dockerJob = appService.getAppDefine("zookeeper"); + List<Map<String, Configmap>> configmapFromPreCollectData = new LinkedList<>(); + for (Metrics metricsDef : dockerJob.getMetrics()) { + metricsDef = CollectUtil.replaceCryPlaceholderToMetrics(metricsDef, configmapFromPreCollectData.size() > 0 ? configmapFromPreCollectData.get(0) : new HashMap<>()); + CollectRep.MetricsData metricsData = validateMetricsCollection(metricsDef, metricsDef.getName()); + configmapFromPreCollectData = CollectUtil.getConfigmapFromPreCollectData(metricsData); + } + } +} diff --git a/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/resources/http/docker/containers_result.txt b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/resources/http/docker/containers_result.txt new file mode 100644 index 0000000..0a45ccc --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/resources/http/docker/containers_result.txt @@ -0,0 +1 @@ +[{"Id":"34174a918eb2e38cdb097c910f74af845e7383b04765d26ad52f940f86342a64","Names":["/mysql"],"Image":"mysql:8.0","ImageID":"sha256:6c55ddbef96911f9f36d1330ffe3f7557c019d49434e738cafabd1a3dd6b4bac","Command":"docker-entrypoint.sh mysqld","Created":1735286602,"Ports":[{"IP":"0.0.0.0","PrivatePort":3306,"PublicPort":3306,"Type":"tcp"},{"IP":"::","PrivatePort":3306,"PublicPort":3306,"Type":"tcp"},{"PrivatePort":33060,"Type":"tcp"}],"Labels":{},"State":"running","Status":"Up 42 hours","HostConfig":{"NetworkMode":"bridge"},"NetworkSettings":{"Networks":{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"MacAddress":"02:42:ac:11:00:04","NetworkID":"305ba4c311d55570760456274cf1653e22bf5383873498e6a159d76baaba0e4e","EndpointID":"6925c9ece99d84ed57c4b08a41c38d009cd8becc9cc2e941eb2b572478a22747","Gateway":"172.17.0.1","IPAddress":"172.17.0.4","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"DriverOpts":null,"DNSNames":null}}},"Mounts":[{"Type":"bind","Source":"/docker/mysql/conf/my.cnf","Destination":"/etc/mysql/my.cnf","Mode":"","RW":true,"Propagation":"rprivate"},{"Type":"bind","Source":"/docker/mysql/data","Destination":"/var/lib/mysql","Mode":"","RW":true,"Propagation":"rprivate"}]},{"Id":"d1f070438d28de7059113df7e79423f5ba1fdc4968cee4914b661cec222ddb06","Names":["/dbgate-instance"],"Image":"dbgate:latest","ImageID":"sha256:107efa35665b98c97cb9698cae2daf3f80aa88854ac90d81bba24b8de02c830d","Command":"/home/dbgate-docker/entrypoint.sh","Created":1734751223,"Ports":[{"IP":"0.0.0.0","PrivatePort":3000,"PublicPort":3000,"Type":"tcp"},{"IP":"::","PrivatePort":3000,"PublicPort":3000,"Type":"tcp"}],"Labels":{"org.opencontainers.image.ref.name":"ubuntu","org.opencontainers.image.version":"22.04"},"State":"running","Status":"Up 7 days","HostConfig":{"NetworkMode":"bridge"},"NetworkSettings":{"Networks":{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"MacAddress":"02:42:ac:11:00:03","NetworkID":"305ba4c311d55570760456274cf1653e22bf5383873498e6a159d76baaba0e4e","EndpointID":"a8f31d5d2ec0100137dced7550b3d662b0804bf8f767a457cbaebda863fb4504","Gateway":"172.17.0.1","IPAddress":"172.17.0.3","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"DriverOpts":null,"DNSNames":null}}},"Mounts":[{"Type":"volume","Name":"d0d9b8e8edce3dc092b2b50a1f9c6bb754fd556cffe95be8ae00e016daaaa3fd","Source":"","Destination":"/root/.dbgate","Driver":"local","Mode":"","RW":true,"Propagation":""}]}] \ No newline at end of file diff --git a/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/resources/http/docker/containers_stats.txt b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/resources/http/docker/containers_stats.txt new file mode 100644 index 0000000..ce75d98 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/resources/http/docker/containers_stats.txt @@ -0,0 +1 @@ +{"read":"2000-01-01T00:00:15.864368408Z","preread":"2000-01-01T00:00:14.86209639Z","pids_stats":{"current":41},"blkio_stats":{"io_service_bytes_recursive":[{"major":8,"minor":0,"op":"Read","value":118784},{"major":8,"minor":0,"op":"Write","value":267395072},{"major":8,"minor":0,"op":"Sync","value":267395072},{"major":8,"minor":0,"op":"Async","value":118784},{"major":8,"minor":0,"op":"Total","value":267513856},{"major":253,"minor":0,"op":"Read","value":118784},{"major":253,"minor":0,"op":"Write","value":267395072},{"major":253,"minor":0,"op":"Sync","value":267395072},{"major":253,"minor":0,"op":"Async","value":118784},{"major":253,"minor":0,"op":"Total","value":267513856}],"io_serviced_recursive":[{"major":8,"minor":0,"op":"Read","value":8},{"major":8,"minor":0,"op":"Write","value":7892},{"major":8,"minor":0,"op":"Sync","value":7892},{"major":8,"minor":0,"op":"Async","value":8},{"major":8,"minor":0,"op":"Total","value":7900},{"major":253,"minor":0,"op":"Read","value":8},{"major":253,"minor":0,"op":"Write","value":7892},{"major":253,"minor":0,"op":"Sync","value":7892},{"major":253,"minor":0,"op":"Async","value":8},{"major":253,"minor":0,"op":"Total","value":7900}],"io_queue_recursive":[],"io_service_time_recursive":[],"io_wait_time_recursive":[],"io_merged_recursive":[],"io_time_recursive":[],"sectors_recursive":[]},"num_procs":0,"storage_stats":{},"cpu_stats":{"cpu_usage":{"total_usage":4810174080874,"percpu_usage":[733251885463,588352569755,649761861064,692942680516,618939038384,513206148155,431772846470,581947051067],"usage_in_kernelmode":1191390000000,"usage_in_usermode":1463160000000},"system_cpu_usage":21196078922904483,"online_cpus":8,"throttling_data":{"periods":0,"throttled_periods":0,"throttled_time":0}},"precpu_stats":{"cpu_usage":{"total_usage":4810165603627,"percpu_usage":[733250739914,588352569755,649761643572,692942567027,618937059553,513205711609,431769258762,581946053435],"usage_in_kernelmode":1191390000000,"usage_in_usermode":1463160000000},"system_cpu_usage":21196070922904483,"online_cpus":8,"throttling_data":{"periods":0,"throttled_periods":0,"throttled_time":0}},"memory_stats":{"usage":505090048,"max_usage":519450624,"stats":{"active_anon":396529664,"active_file":52400128,"cache":108560384,"dirty":0,"hierarchical_memory_limit":9223372036854771712,"hierarchical_memsw_limit":9223372036854771712,"inactive_anon":0,"inactive_file":56160256,"mapped_file":40960,"pgfault":409148,"pgmajfault":0,"pgpgin":343733,"pgpgout":220420,"rss":396529664,"rss_huge":0,"total_active_anon":396529664,"total_active_file":52400128,"total_cache":108560384,"total_dirty":0,"total_inactive_anon":0,"total_inactive_file":56160256,"total_mapped_file":40960,"total_pgfault":0,"total_pgmajfault":0,"total_pgpgin":0,"total_pgpgout":0,"total_rss":396529664,"total_rss_huge":0,"total_unevictable":0,"total_writeback":0,"unevictable":0,"writeback":0},"limit":33566269440},"name":"/mysql","id":"34174a918eb2e38cdb097c910f74af845e7383b04765d26ad52f940f86342a64","networks":{"eth0":{"rx_bytes":68182,"rx_packets":533,"rx_errors":0,"rx_dropped":0,"tx_bytes":211850,"tx_packets":361,"tx_errors":0,"tx_dropped":0}}} \ No newline at end of file diff --git a/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/resources/http/docker/system_result.txt b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/resources/http/docker/system_result.txt new file mode 100644 index 0000000..c26a69a --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/resources/http/docker/system_result.txt @@ -0,0 +1 @@ +{"ID":"8483c578-3364-4c9e-914f-bd171d8d9a8e","Containers":3,"ContainersRunning":3,"ContainersPaused":0,"ContainersStopped":0,"Images":13,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"],["userxattr","false"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","ipvlan","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","local","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemoryTCP":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":true,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":false,"NFd":47,"OomKillDisable":true,"NGoroutines":63,"SystemTime":"2024-12-29T14:23:11.284383336+08:00","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","CgroupVersion":"1","NEventsListener":0,"KernelVersion":"3.10.0-1160.71.1.el7.x86_64","OperatingSystem":"CentOS Linux 7 (Core)","OSVersion":"7","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":["https://exinp.mirror.aliyuncs.com/"],"Secure":true,"Official":true}},"Mirrors":["https://exinp.mirror.aliyuncs.com/"]},"NCPU":8,"MemTotal":33566269440,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"bigdata-new-25-214","Labels":[],"ExperimentalBuild":false,"ServerVersion":"26.1.4","Runtimes":{"io.containerd.runc.v2":{"path":"runc","status":{"org.opencontainers.runtime-spec.features":"{\"ociVersionMin\":\"1.0.0\",\"ociVersionMax\":\"1.0.2-dev\",\"hooks\":[\"prestart\",\"createRuntime\",\"createContainer\",\"startContainer\",\"poststart\",\"poststop\"],\"mountOptions\":[\"acl\",\"async\",\"atime\",\"bind\",\"defaults\",\"dev\",\"diratime\",\"dirsync\",\"exec\",\"iversion\",\"lazytime\",\"loud\",\"mand\",\"noacl\",\"noatime\",\"nodev\",\"nodiratime\",\"noexec\",\"noiversion\",\"nolazytime\",\"nomand\",\"norelatime\",\"nostrictatime\",\"nosuid\",\"nosymfollow\",\"private\",\"ratime\",\"rbind\",\"rdev\",\"rdiratime\",\"relatime\",\"remount\",\"rexec\",\"rnoatime\",\"rnodev\",\"rnodiratime\",\"rnoexec\",\"rnorelatime\",\"rnostrictatime\",\"rnosuid\",\"rnosymfollow\",\"ro\",\"rprivate\",\"rrelatime\",\"rro\",\"rrw\",\"rshared\",\"rslave\",\"rstrictatime\",\"rsuid\",\"rsymfollow\",\"runbindable\",\"rw\",\"shared\",\"silent\",\"slave\",\"strictatime\",\"suid\",\"symfollow\",\"sync\",\"tmpcopyup\",\"unbindable\"],\"linux\":{\"namespaces\":[\"cgroup\",\"ipc\",\"mount\",\"network\",\"pid\",\"user\",\"uts\"],\"capabilities\":[\"CAP_CHOWN\",\"CAP_DAC_OVERRIDE\",\"CAP_DAC_READ_SEARCH\",\"CAP_FOWNER\",\"CAP_FSETID\",\"CAP_KILL\",\"CAP_SETGID\",\"CAP_SETUID\",\"CAP_SETPCAP\",\"CAP_LINUX_IMMUTABLE\",\"CAP_NET_BIND_SERVICE\",\"CAP_NET_BROADCAST\",\"CAP_NET_ADMIN\",\"CAP_NET_RAW\",\"CAP_IPC_LOCK\",\"CAP_IPC_OWNER\",\"CAP_SYS_MODULE\",\"CAP_SYS_RAWIO\",\"CAP_SYS_CHROOT\",\"CAP_SYS_PTRACE\",\"CAP_SYS_PACCT\",\"CAP_SYS_ADMIN\",\"CAP_SYS_BOOT\",\"CAP_SYS_NICE\",\"CAP_SYS_RESOURCE\",\"CAP_SYS_TIME\",\"CAP_SYS_TTY_CONFIG\",\"CAP_MKNOD\",\"CAP_LEASE\",\"CAP_AUDIT_WRITE\",\"CAP_AUDIT_CONTROL\",\"CAP_SETFCAP\",\"CAP_MAC_OVERRIDE\",\"CAP_MAC_ADMIN\",\"CAP_SYSLOG\",\"CAP_WAKE_ALARM\",\"CAP_BLOCK_SUSPEND\",\"CAP_AUDIT_READ\",\"CAP_PERFMON\",\"CAP_BPF\",\"CAP_CHECKPOINT_RESTORE\"],\"cgroup\":{\"v1\":true,\"v2\":true,\"systemd\":true,\"systemdUser\":true},\"seccomp\":{\"enabled\":true,\"actions\":[\"SCMP_ACT_ALLOW\",\"SCMP_ACT_ERRNO\",\"SCMP_ACT_KILL\",\"SCMP_ACT_KILL_PROCESS\",\"SCMP_ACT_KILL_THREAD\",\"SCMP_ACT_LOG\",\"SCMP_ACT_NOTIFY\",\"SCMP_ACT_TRACE\",\"SCMP_ACT_TRAP\"],\"operators\":[\"SCMP_CMP_EQ\",\"SCMP_CMP_GE\",\"SCMP_CMP_GT\",\"SCMP_CMP_LE\",\"SCMP_CMP_LT\",\"SCMP_CMP_MASKED_EQ\",\"SCMP_CMP_NE\"],\"archs\":[\"SCMP_ARCH_AARCH64\",\"SCMP_ARCH_ARM\",\"SCMP_ARCH_MIPS\",\"SCMP_ARCH_MIPS64\",\"SCMP_ARCH_MIPS64N32\",\"SCMP_ARCH_MIPSEL\",\"SCMP_ARCH_MIPSEL64\",\"SCMP_ARCH_MIPSEL64N32\",\"SCMP_ARCH_PPC\",\"SCMP_ARCH_PPC64\",\"SCMP_ARCH_PPC64LE\",\"SCMP_ARCH_RISCV64\",\"SCMP_ARCH_S390\",\"SCMP_ARCH_S390X\",\"SCMP_ARCH_X32\",\"SCMP_ARCH_X86\",\"SCMP_ARCH_X86_64\"]},\"apparmor\":{\"enabled\":true},\"selinux\":{\"enabled\":true}},\"annotations\":{\"io.github.seccomp.libseccomp.version\":\"2.3.1\",\"org.opencontainers.runc.checkpoint.enabled\":\"true\",\"org.opencontainers.runc.commit\":\"v1.1.12-0-g51d5e94\",\"org.opencontainers.runc.version\":\"1.1.12\"}}"}},"runc":{"path":"runc","status":{"org.opencontainers.runtime-spec.features":"{\"ociVersionMin\":\"1.0.0\",\"ociVersionMax\":\"1.0.2-dev\",\"hooks\":[\"prestart\",\"createRuntime\",\"createContainer\",\"startContainer\",\"poststart\",\"poststop\"],\"mountOptions\":[\"acl\",\"async\",\"atime\",\"bind\",\"defaults\",\"dev\",\"diratime\",\"dirsync\",\"exec\",\"iversion\",\"lazytime\",\"loud\",\"mand\",\"noacl\",\"noatime\",\"nodev\",\"nodiratime\",\"noexec\",\"noiversion\",\"nolazytime\",\"nomand\",\"norelatime\",\"nostrictatime\",\"nosuid\",\"nosymfollow\",\"private\",\"ratime\",\"rbind\",\"rdev\",\"rdiratime\",\"relatime\",\"remount\",\"rexec\",\"rnoatime\",\"rnodev\",\"rnodiratime\",\"rnoexec\",\"rnorelatime\",\"rnostrictatime\",\"rnosuid\",\"rnosymfollow\",\"ro\",\"rprivate\",\"rrelatime\",\"rro\",\"rrw\",\"rshared\",\"rslave\",\"rstrictatime\",\"rsuid\",\"rsymfollow\",\"runbindable\",\"rw\",\"shared\",\"silent\",\"slave\",\"strictatime\",\"suid\",\"symfollow\",\"sync\",\"tmpcopyup\",\"unbindable\"],\"linux\":{\"namespaces\":[\"cgroup\",\"ipc\",\"mount\",\"network\",\"pid\",\"user\",\"uts\"],\"capabilities\":[\"CAP_CHOWN\",\"CAP_DAC_OVERRIDE\",\"CAP_DAC_READ_SEARCH\",\"CAP_FOWNER\",\"CAP_FSETID\",\"CAP_KILL\",\"CAP_SETGID\",\"CAP_SETUID\",\"CAP_SETPCAP\",\"CAP_LINUX_IMMUTABLE\",\"CAP_NET_BIND_SERVICE\",\"CAP_NET_BROADCAST\",\"CAP_NET_ADMIN\",\"CAP_NET_RAW\",\"CAP_IPC_LOCK\",\"CAP_IPC_OWNER\",\"CAP_SYS_MODULE\",\"CAP_SYS_RAWIO\",\"CAP_SYS_CHROOT\",\"CAP_SYS_PTRACE\",\"CAP_SYS_PACCT\",\"CAP_SYS_ADMIN\",\"CAP_SYS_BOOT\",\"CAP_SYS_NICE\",\"CAP_SYS_RESOURCE\",\"CAP_SYS_TIME\",\"CAP_SYS_TTY_CONFIG\",\"CAP_MKNOD\",\"CAP_LEASE\",\"CAP_AUDIT_WRITE\",\"CAP_AUDIT_CONTROL\",\"CAP_SETFCAP\",\"CAP_MAC_OVERRIDE\",\"CAP_MAC_ADMIN\",\"CAP_SYSLOG\",\"CAP_WAKE_ALARM\",\"CAP_BLOCK_SUSPEND\",\"CAP_AUDIT_READ\",\"CAP_PERFMON\",\"CAP_BPF\",\"CAP_CHECKPOINT_RESTORE\"],\"cgroup\":{\"v1\":true,\"v2\":true,\"systemd\":true,\"systemdUser\":true},\"seccomp\":{\"enabled\":true,\"actions\":[\"SCMP_ACT_ALLOW\",\"SCMP_ACT_ERRNO\",\"SCMP_ACT_KILL\",\"SCMP_ACT_KILL_PROCESS\",\"SCMP_ACT_KILL_THREAD\",\"SCMP_ACT_LOG\",\"SCMP_ACT_NOTIFY\",\"SCMP_ACT_TRACE\",\"SCMP_ACT_TRAP\"],\"operators\":[\"SCMP_CMP_EQ\",\"SCMP_CMP_GE\",\"SCMP_CMP_GT\",\"SCMP_CMP_LE\",\"SCMP_CMP_LT\",\"SCMP_CMP_MASKED_EQ\",\"SCMP_CMP_NE\"],\"archs\":[\"SCMP_ARCH_AARCH64\",\"SCMP_ARCH_ARM\",\"SCMP_ARCH_MIPS\",\"SCMP_ARCH_MIPS64\",\"SCMP_ARCH_MIPS64N32\",\"SCMP_ARCH_MIPSEL\",\"SCMP_ARCH_MIPSEL64\",\"SCMP_ARCH_MIPSEL64N32\",\"SCMP_ARCH_PPC\",\"SCMP_ARCH_PPC64\",\"SCMP_ARCH_PPC64LE\",\"SCMP_ARCH_RISCV64\",\"SCMP_ARCH_S390\",\"SCMP_ARCH_S390X\",\"SCMP_ARCH_X32\",\"SCMP_ARCH_X86\",\"SCMP_ARCH_X86_64\"]},\"apparmor\":{\"enabled\":true},\"selinux\":{\"enabled\":true}},\"annotations\":{\"io.github.seccomp.libseccomp.version\":\"2.3.1\",\"org.opencontainers.runc.checkpoint.enabled\":\"true\",\"org.opencontainers.runc.commit\":\"v1.1.12-0-g51d5e94\",\"org.opencontainers.runc.version\":\"1.1.12\"}}"}}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"d2d58213f83a351ca8f528a95fbd145f5654e957","Expected":"d2d58213f83a351ca8f528a95fbd145f5654e957"},"RuncCommit":{"ID":"v1.1.12-0-g51d5e94","Expected":"v1.1.12-0-g51d5e94"},"InitCommit":{"ID":"de40ad0","Expected":"de40ad0"},"SecurityOptions":["name=seccomp,profile=builtin"],"CDISpecDirs":[],"Warnings":["[DEPRECATION NOTICE]: API is accessible on http://0.0.0.0:2375 without encryption.\n Access to the remote API is equivalent to root access on the host. Refer\n to the 'Docker daemon attack surface' section in the documentation for\n more information: https://docs.docker.com/go/attack-surface/\nIn future versions this will be a hard failure preventing the daemon from starting! Learn more at: https://docs.docker.com/go/api-security/"]} \ No newline at end of file diff --git a/hertzbeat-e2e/hertzbeat-collector-common-e2e/pom.xml b/hertzbeat-e2e/hertzbeat-collector-common-e2e/pom.xml new file mode 100644 index 0000000..1e2952b --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-common-e2e/pom.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-e2e</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-collector-common-e2e</artifactId> + + <properties> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-basic</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-common</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>${maven-jar-plugin.version}</version> + <configuration> + <skip>false</skip> + </configuration> + <executions> + <execution> + <goals> + <goal>test-jar</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/hertzbeat-e2e/hertzbeat-collector-common-e2e/src/test/java/org/apache/hertzbeat/collector/collect/AbstractCollectE2eTest.java b/hertzbeat-e2e/hertzbeat-collector-common-e2e/src/test/java/org/apache/hertzbeat/collector/collect/AbstractCollectE2eTest.java new file mode 100644 index 0000000..b58a442 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-common-e2e/src/test/java/org/apache/hertzbeat/collector/collect/AbstractCollectE2eTest.java @@ -0,0 +1,158 @@ +/* + * 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.collector.collect; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.dispatch.CollectDataDispatch; +import org.apache.hertzbeat.collector.dispatch.MetricsCollect; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.collector.timer.WheelTimerTask; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.Protocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.manager.dao.DefineDao; +import org.apache.hertzbeat.manager.service.impl.AppServiceImpl; +import org.apache.hertzbeat.manager.service.impl.ObjectStoreConfigServiceImpl; +import org.junit.jupiter.api.Assertions; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * AbstractCollectE2eTest + */ +@Slf4j +public abstract class AbstractCollectE2eTest { + + @InjectMocks + protected AppServiceImpl appService; + + protected AbstractCollect collect; + + protected MetricsCollect metricsCollect; + + protected Metrics metrics; + + @Mock + protected ObjectStoreConfigServiceImpl objectStoreConfigService; + @Mock + private WheelTimerTask timerJob; + @Mock + private Timeout timeout; + @Mock + private Job job; + @Mock + private DefineDao defineDao; + + public void setUp() throws Exception { + // Initialize mocks + MockitoAnnotations.openMocks(this); + lenient().when(defineDao.findAll()).thenReturn(new ArrayList<>()); + when(timeout.task()).thenReturn(timerJob); + when(timerJob.getJob()).thenReturn(job); + metricsCollect = new MetricsCollect(mock(Metrics.class), timeout, mock(CollectDataDispatch.class), null, List.of()); + + // Initialize services and components + appService.afterPropertiesSet(); + metrics = new Metrics(); + } + + /** + * Validate metrics collection, check if the metrics values are not empty <br/> + * + * @param metricsDef metrics definition + * @param metricName metric name + * @return metrics data + */ + protected CollectRep.MetricsData validateMetricsCollection(Metrics metricsDef, String metricName) { + // By default, we do not allow empty values + return validateMetricsCollection(metricsDef, metricName, false); + } + + /** + * Validate metrics collection, check if the metrics values are not empty <br/> + * We believe that all monitoring metrics should have data + * + * @param metricsDef metrics definition + * @param metricName metric name + * @param allowEmpty In some special scenarios, it is not necessary to check if the value is ` ` + */ + protected CollectRep.MetricsData validateMetricsCollection(Metrics metricsDef, String metricName, boolean allowEmpty) { + CollectRep.MetricsData.Builder metricsData = collectMetrics(metricsDef); + + metricsCollect.calculateFields(metricsDef, metricsData); + + Assertions.assertTrue(metricsData.getValuesList().size() > 0, + String.format("%s metrics values should not be empty, detail: %s", metricName, metricsData.getMsg())); + + for (CollectRep.ValueRow valueRow : metricsData.getValuesList()) { + for (int i = 0; i < valueRow.getColumnsCount(); i++) { + Assertions.assertFalse(valueRow.getColumns(i).isEmpty(), + String.format("%s metric column %d should not be empty", metricName, i)); + if (!allowEmpty) { + // Check if the value is not null + Assertions.assertNotEquals(CommonConstants.NULL_VALUE, valueRow.getColumns(i), String.format("%s metric column %d should not be null", metricName, i)); + } + } + } + + log.info("{} metrics validation passed", metricName); + return metricsData.build(); + } + + /** + * Set alias fields for metrics + * + * @param metrics metrics + * @param metricsDef metrics definition + */ + protected void setMetricsAliasFields(Metrics metrics, Metrics metricsDef) { + List<String> aliasFields = metricsDef.getAliasFields() == null + ? metricsDef.getFields().stream().map(Metrics.Field::getField).collect(Collectors.toList()) + : metricsDef.getAliasFields(); + metrics.setAliasFields(aliasFields); + metricsDef.setAliasFields(aliasFields); + } + + protected abstract CollectRep.MetricsData.Builder collectMetrics(Metrics metricsDef); + + protected CollectRep.MetricsData.Builder collectMetricsData(Metrics metrics, Metrics metricsDef) { + CollectRep.MetricsData.Builder metricsData = CollectRep.MetricsData.newBuilder(); + return this.collectMetricsData(metrics, metricsDef, metricsData); + } + + protected CollectRep.MetricsData.Builder collectMetricsData(Metrics metrics, Metrics metricsDef, CollectRep.MetricsData.Builder metricsData) { + setMetricsAliasFields(metrics, metricsDef); + + // Collect metrics + collect.collect(metricsData, metrics); + return metricsData; + } + + protected abstract Protocol buildProtocol(Metrics metricsDef); +} diff --git a/hertzbeat-e2e/hertzbeat-collector-kafka-e2e/pom.xml b/hertzbeat-e2e/hertzbeat-collector-kafka-e2e/pom.xml new file mode 100644 index 0000000..3c18249 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-kafka-e2e/pom.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-e2e</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-collector-kafka-e2e</artifactId> + + <properties> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers-kafka</artifactId> + <version>${testcontainers.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.kafka</groupId> + <artifactId>kafka-clients</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-kafka</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-common</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/hertzbeat-e2e/hertzbeat-collector-kafka-e2e/src/test/java/org/apache/hertzbeat/collector/collect/kafka/KafkaCollectE2eTest.java b/hertzbeat-e2e/hertzbeat-collector-kafka-e2e/src/test/java/org/apache/hertzbeat/collector/collect/kafka/KafkaCollectE2eTest.java new file mode 100644 index 0000000..7454c90 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-kafka-e2e/src/test/java/org/apache/hertzbeat/collector/collect/kafka/KafkaCollectE2eTest.java @@ -0,0 +1,145 @@ +/* + * 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.collector.collect.kafka; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.KafkaProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.KafkaAdminClient; +import org.apache.kafka.clients.admin.NewTopic; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.lifecycle.Startables; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.DockerLoggerFactory; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; + +/** + * KafkaCollectE2E + */ +@Slf4j +public class KafkaCollectE2eTest { + + private static final String ZOOKEEPER_IMAGE_NAME = "zookeeper:3.8.4"; + private static final String ZOOKEEPER_NAME = "zookeeper"; + private static final Integer ZOOKEEPER_PORT = 2181; + private static final String KAFKA_IMAGE_NAME = "confluentinc/cp-kafka:7.4.7"; + private static final String KAFKA_NAME = "kafka"; + private static GenericContainer<?> zookeeperContainer; + private static KafkaContainer kafkaContainer; + private Metrics metrics; + private KafkaCollectImpl kafkaCollect; + private CollectRep.MetricsData.Builder builder; + + @AfterAll + public static void tearDown() { + kafkaContainer.stop(); + zookeeperContainer.stop(); + } + + @BeforeEach + public void setUp() { + kafkaCollect = new KafkaCollectImpl(); + metrics = new Metrics(); + Network.NetworkImpl network = Network.builder().build(); + zookeeperContainer = new GenericContainer<>(DockerImageName.parse(ZOOKEEPER_IMAGE_NAME)) + .withExposedPorts(ZOOKEEPER_PORT) + .withNetwork(network) + .withNetworkAliases(ZOOKEEPER_NAME) + .waitingFor(Wait.forListeningPort()) + .withStartupTimeout(Duration.ofSeconds(120)); + zookeeperContainer.setPortBindings(Collections.singletonList(ZOOKEEPER_PORT + ":" + ZOOKEEPER_PORT)); + + Startables.deepStart(Stream.of(zookeeperContainer)).join(); + + kafkaContainer = new KafkaContainer(DockerImageName.parse(KAFKA_IMAGE_NAME)) + .withExternalZookeeper(ZOOKEEPER_NAME + ":2181") + .withNetwork(network) + .withNetworkAliases(KAFKA_NAME) + .withLogConsumer( + new Slf4jLogConsumer( + DockerLoggerFactory.getLogger(KAFKA_IMAGE_NAME))) + .withStartupTimeout(Duration.ofSeconds(120)); + Startables.deepStart(Stream.of(kafkaContainer)).join(); + } + + @Test + public void testKafkaCollect() throws ExecutionException, InterruptedException, TimeoutException { + + Assertions.assertTrue(zookeeperContainer.isRunning(), "Zookeeper container should be running"); + Assertions.assertTrue(kafkaContainer.isRunning(), "Kafka container should be running"); + + String topicName = "test-topic"; + + String bootstrapServers = kafkaContainer.getBootstrapServers().replace("PLAINTEXT://", ""); + KafkaProtocol kafkaProtocol = new KafkaProtocol(); + kafkaProtocol.setHost(bootstrapServers.split(":")[0]); + kafkaProtocol.setPort(bootstrapServers.split(":")[1]); + kafkaProtocol.setCommand("topic-list"); + metrics.setKclient(kafkaProtocol); + log.info("bootstrapServers: {}", bootstrapServers); + + // Create Topic + Properties properties = new Properties(); + properties.put("bootstrap.servers", bootstrapServers); + int numPartitions = 1; + short replicationFactor = 1; + try (AdminClient adminClient = KafkaAdminClient.create(properties)) { + NewTopic newTopic = new NewTopic(topicName, numPartitions, replicationFactor); + adminClient.createTopics(Collections.singletonList(newTopic)).all().get(60, TimeUnit.SECONDS); + } + + // Verify the information of topic list monitoring + builder = CollectRep.MetricsData.newBuilder(); + kafkaCollect.collect(builder, metrics); + Assertions.assertTrue(builder.getValuesList().stream() + .anyMatch(valueRow -> valueRow.getColumns(0).equals(topicName))); + + // Verify the information monitored by topic description + builder = CollectRep.MetricsData.newBuilder(); + kafkaProtocol.setCommand("topic-describe"); + kafkaCollect.collect(builder, metrics); + List<CollectRep.ValueRow> topicDescribeList = builder.getValuesList(); + CollectRep.ValueRow firstRow = topicDescribeList.get(0); + Assertions.assertAll( + () -> Assertions.assertEquals(topicName, firstRow.getColumns(0)), + () -> Assertions.assertEquals(String.valueOf(numPartitions), firstRow.getColumns(1)), + () -> Assertions.assertEquals("0", firstRow.getColumns(2)), + () -> Assertions.assertEquals(kafkaProtocol.getHost(), firstRow.getColumns(3)), + () -> Assertions.assertEquals(kafkaProtocol.getPort(), firstRow.getColumns(4)), + () -> Assertions.assertEquals(String.valueOf(replicationFactor), firstRow.getColumns(5)) + ); + } +} diff --git a/hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e/pom.xml b/hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e/pom.xml new file mode 100644 index 0000000..49b2f51 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e/pom.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-e2e</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-collector-mysql-r2dbc-e2e</artifactId> + + <properties> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <mysql.r2dbc.reactor.bom.version>2024.0.3</mysql.r2dbc.reactor.bom.version> + </properties> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>io.projectreactor</groupId> + <artifactId>reactor-bom</artifactId> + <version>${mysql.r2dbc.reactor.bom.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-startup</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>com.mysql</groupId> + <artifactId>mysql-connector-j</artifactId> + </exclusion> + <exclusion> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-webflux</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-common-e2e</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + <type>test-jar</type> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-basic</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-common</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-collector</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.projectreactor.netty</groupId> + <artifactId>reactor-netty-core</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers-junit-jupiter</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers-mysql</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <systemPropertyVariables> + <!-- Force Reactor Netty onto the portable NIO path in this E2E so startup's WebFlux stack + cannot pull the test onto a macOS-native transport variant. --> + <reactor.netty.native>false</reactor.netty.native> + </systemPropertyVariables> + </configuration> + </plugin> + </plugins> + </build> +</project> diff --git a/hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e/src/test/java/org/apache/hertzbeat/collector/collect/mysql/AbstractMysqlR2dbcCollectE2eTest.java b/hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e/src/test/java/org/apache/hertzbeat/collector/collect/mysql/AbstractMysqlR2dbcCollectE2eTest.java new file mode 100644 index 0000000..aff3ffb --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e/src/test/java/org/apache/hertzbeat/collector/collect/mysql/AbstractMysqlR2dbcCollectE2eTest.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.mysql; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.hertzbeat.collector.collect.AbstractCollectE2eTest; +import org.apache.hertzbeat.collector.collect.database.JdbcCommonCollect; +import org.apache.hertzbeat.collector.collect.database.mysql.MysqlCollectorProperties; +import org.apache.hertzbeat.collector.collect.database.mysql.MysqlJdbcDriverAvailability; +import org.apache.hertzbeat.collector.collect.database.mysql.MysqlR2dbcJdbcQueryExecutor; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcConnectionFactoryProvider; +import org.apache.hertzbeat.collector.mysql.r2dbc.MysqlR2dbcQueryExecutor; +import org.apache.hertzbeat.collector.mysql.r2dbc.ResultSetMapper; +import org.apache.hertzbeat.collector.mysql.r2dbc.SqlGuard; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.job.protocol.Protocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.Assertions; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +/** + * Shared MySQL-compatible E2E support for the collector-side R2DBC adapter. + */ +abstract class AbstractMysqlR2dbcCollectE2eTest extends AbstractCollectE2eTest { + + protected static final String TEST_DATABASE = "hzb"; + protected static final String TEST_USERNAME = "test"; + protected static final String TEST_PASSWORD = "test123"; + protected static final String ROOT_PASSWORD = "root123"; + + protected GenericContainer<?> container; + private MysqlR2dbcJdbcQueryExecutor jdbcQueryExecutor; + + protected void setUpTarget(DatabaseTarget target) throws Exception { + super.setUp(); + collect = new JdbcCommonCollect(); + metrics = new Metrics(); + + container = createContainer(target); + container.start(); + awaitTcpLoginReady(); + initMonitoringData(); + + MysqlCollectorProperties properties = new MysqlCollectorProperties(); + properties.setQueryEngine(MysqlCollectorProperties.QueryEngine.R2DBC); + jdbcQueryExecutor = new MysqlR2dbcJdbcQueryExecutor( + properties, + new MysqlR2dbcQueryExecutor( + new MysqlR2dbcConnectionFactoryProvider(), + new ResultSetMapper(), + new SqlGuard()), + new MysqlJdbcDriverAvailability()); + jdbcQueryExecutor.afterPropertiesSet(); + } + + protected void tearDownTarget() throws Exception { + if (jdbcQueryExecutor != null) { + jdbcQueryExecutor.destroy(); + jdbcQueryExecutor = null; + } + if (container != null) { + container.stop(); + container = null; + } + } + + protected void assertMysqlJdbcDriverAbsent() { + Assertions.assertThrows(ClassNotFoundException.class, () -> Class.forName("com.mysql.cj.jdbc.Driver")); + } + + protected void collectMysqlTemplate(Set<String> metricFilter) throws Exception { + Job mysqlJob = appService.getAppDefine("mysql"); + List<Map<String, Configmap>> configmapFromPreCollectData = new LinkedList<>(); + for (Metrics metricsDef : mysqlJob.getMetrics()) { + if (metricFilter != null && !metricFilter.contains(metricsDef.getName())) { + continue; + } + metricsDef = CollectUtil.replaceCryPlaceholderToMetrics(metricsDef, + configmapFromPreCollectData.isEmpty() ? new HashMap<>() : configmapFromPreCollectData.getFirst()); + String metricName = metricsDef.getName(); + if ("process_state".equals(metricName)) { + startBackgroundSleepQuery(); + } + if ("slow_sql".equals(metricName)) { + generateSlowQuery(); + } + CollectRep.MetricsData metricsData = validateMetricsCollection(metricsDef, metricName, true); + configmapFromPreCollectData = CollectUtil.getConfigmapFromPreCollectData(metricsData); + } + } + + @Override + protected CollectRep.MetricsData.Builder collectMetrics(Metrics metricsDef) { + JdbcProtocol jdbcProtocol = (JdbcProtocol) buildProtocol(metricsDef); + metrics.setJdbc(jdbcProtocol); + CollectRep.MetricsData.Builder metricsData = CollectRep.MetricsData.newBuilder(); + metricsData.setApp("mysql"); + return collectMetricsData(metrics, metricsDef, metricsData); + } + + @Override + protected Protocol buildProtocol(Metrics metricsDef) { + JdbcProtocol jdbcProtocol = metricsDef.getJdbc(); + jdbcProtocol.setHost(container.getHost()); + jdbcProtocol.setPort(String.valueOf(container.getMappedPort(3306))); + jdbcProtocol.setUsername(TEST_USERNAME); + jdbcProtocol.setPassword(TEST_PASSWORD); + jdbcProtocol.setDatabase(TEST_DATABASE); + jdbcProtocol.setTimeout("8000"); + jdbcProtocol.setReuseConnection("false"); + jdbcProtocol.setUrl(null); + jdbcProtocol.setSshTunnel(null); + return jdbcProtocol; + } + + private GenericContainer<?> createContainer(DatabaseTarget target) { + GenericContainer<?> mysql = new GenericContainer<>(target.image()) + .withExposedPorts(3306) + .waitingFor(Wait.forListeningPort()); + if (target.mariaDb()) { + return mysql.withEnv("MARIADB_DATABASE", TEST_DATABASE) + .withEnv("MARIADB_USER", TEST_USERNAME) + .withEnv("MARIADB_PASSWORD", TEST_PASSWORD) + .withEnv("MARIADB_ROOT_PASSWORD", ROOT_PASSWORD); + } + return mysql.withEnv("MYSQL_DATABASE", TEST_DATABASE) + .withEnv("MYSQL_USER", TEST_USERNAME) + .withEnv("MYSQL_PASSWORD", TEST_PASSWORD) + .withEnv("MYSQL_ROOT_PASSWORD", ROOT_PASSWORD); + } + + private void initMonitoringData() throws Exception { + execRoot("GRANT SELECT ON mysql.* TO '" + TEST_USERNAME + "'@'%';" + + " GRANT PROCESS ON *.* TO '" + TEST_USERNAME + "'@'%';" + + " SET GLOBAL log_output='TABLE';" + + " SET GLOBAL slow_query_log='ON';" + + " SET GLOBAL long_query_time=0;" + + " FLUSH PRIVILEGES;"); + generateSlowQuery(); + } + + private void generateSlowQuery() throws Exception { + execUser(TEST_DATABASE, "SELECT SLEEP(0.2);"); + Thread.sleep(300); + } + + private void startBackgroundSleepQuery() throws Exception { + String command = String.join(" ", + "CLIENT=$(command -v mysql || command -v mariadb)", + "&&", + "nohup sh -lc", + "'$CLIENT --protocol=TCP -h127.0.0.1 -P3306", + "-u" + TEST_USERNAME, + "-p" + TEST_PASSWORD, + TEST_DATABASE, + "-e", + "\"SELECT SLEEP(15)\" >/tmp/process-state.log 2>&1'", + ">/dev/null 2>&1 &"); + container.execInContainer("sh", "-lc", command); + Thread.sleep(500); + } + + private void awaitTcpLoginReady() throws Exception { + long deadline = System.currentTimeMillis() + 30_000L; + while (System.currentTimeMillis() < deadline) { + try { + var result = container.execInContainer("sh", "-lc", + mysqlCliCommand(TEST_USERNAME, TEST_PASSWORD, TEST_DATABASE, "SELECT 1")); + if (result.getExitCode() == 0) { + return; + } + } catch (Exception ignored) { + // Wait for the MySQL entrypoint to finish bootstrapping and switch to the final TCP listener. + } + Thread.sleep(1000); + } + throw new IllegalStateException("Timed out waiting for MySQL-compatible TCP login to become ready"); + } + + private void execRoot(String sql) throws Exception { + var result = container.execInContainer("sh", "-lc", mysqlCliCommand("root", ROOT_PASSWORD, "mysql", sql)); + if (result.getExitCode() != 0) { + throw new IllegalStateException("root mysql command failed: " + result.getStderr()); + } + } + + private void execUser(String database, String sql) throws Exception { + var result = container.execInContainer("sh", "-lc", mysqlCliCommand(TEST_USERNAME, TEST_PASSWORD, database, sql)); + if (result.getExitCode() != 0) { + throw new IllegalStateException("user mysql command failed: " + result.getStderr()); + } + } + + private String mysqlCliCommand(String username, String password, String database, String sql) { + return String.join(" ", + "CLIENT=$(command -v mysql || command -v mariadb)", + "&&", + "$CLIENT --protocol=TCP -h127.0.0.1 -P3306", + "-u" + username, + "-p" + password, + database, + "-e", + "\"" + sql.replace("\"", "\\\"") + "\""); + } + + protected record DatabaseTarget(String name, DockerImageName image, boolean mariaDb) { + } +} diff --git a/hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e/src/test/java/org/apache/hertzbeat/collector/collect/mysql/MysqlR2dbcCollectCompatibilityE2eTest.java b/hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e/src/test/java/org/apache/hertzbeat/collector/collect/mysql/MysqlR2dbcCollectCompatibilityE2eTest.java new file mode 100644 index 0000000..7c34f8a --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e/src/test/java/org/apache/hertzbeat/collector/collect/mysql/MysqlR2dbcCollectCompatibilityE2eTest.java @@ -0,0 +1,61 @@ +/* + * 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.collector.collect.mysql; + +import java.util.Set; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.testcontainers.utility.DockerImageName; + +/** + * Compatibility E2E coverage for the collector-side MySQL R2DBC adapter. + */ +class MysqlR2dbcCollectCompatibilityE2eTest extends AbstractMysqlR2dbcCollectE2eTest { + + private static final Set<String> MARIADB_REPRESENTATIVE_METRICS = + Set.of("basic", "process_state", "slow_sql"); + + @TestFactory + Stream<DynamicTest> shouldCollectMysqlTemplateAcrossCompatibilityMatrixWithoutMysqlJdbcDriver() { + return Stream.of( + new MatrixTarget( + new DatabaseTarget("mysql-5.7.44", DockerImageName.parse("mysql:5.7.44"), false), + null), + new MatrixTarget( + new DatabaseTarget("mysql-8.0.36", DockerImageName.parse("mysql:8.0.36"), false), + null), + new MatrixTarget( + new DatabaseTarget("mariadb-11.4", DockerImageName.parse("mariadb:11.4"), true), + MARIADB_REPRESENTATIVE_METRICS)) + .map(target -> DynamicTest.dynamicTest(target.databaseTarget().name(), () -> verifyTarget(target))); + } + + private void verifyTarget(MatrixTarget target) throws Exception { + setUpTarget(target.databaseTarget()); + try { + assertMysqlJdbcDriverAbsent(); + collectMysqlTemplate(target.metricFilter()); + } finally { + tearDownTarget(); + } + } + + private record MatrixTarget(DatabaseTarget databaseTarget, Set<String> metricFilter) { + } +} diff --git a/hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e/src/test/java/org/apache/hertzbeat/collector/collect/mysql/MysqlR2dbcCollectE2eTest.java b/hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e/src/test/java/org/apache/hertzbeat/collector/collect/mysql/MysqlR2dbcCollectE2eTest.java new file mode 100644 index 0000000..9bd4eba --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e/src/test/java/org/apache/hertzbeat/collector/collect/mysql/MysqlR2dbcCollectE2eTest.java @@ -0,0 +1,41 @@ +/* + * 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.collector.collect.mysql; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.testcontainers.utility.DockerImageName; + +/** + * E2E test for collector-side MySQL monitoring through the R2DBC query adapter. + */ +@Slf4j +class MysqlR2dbcCollectE2eTest extends AbstractMysqlR2dbcCollectE2eTest { + + @Test + void shouldCollectMysqlTemplateWithoutMysqlJdbcDriver() throws Exception { + DatabaseTarget target = new DatabaseTarget("mysql-8.0.36", DockerImageName.parse("mysql:8.0.36"), false); + setUpTarget(target); + try { + assertMysqlJdbcDriverAbsent(); + collectMysqlTemplate(null); + } finally { + tearDownTarget(); + } + } +} diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/pom.xml b/hertzbeat-e2e/hertzbeat-log-e2e/pom.xml new file mode 100644 index 0000000..a5638e2 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-log-e2e/pom.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-e2e</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>hertzbeat-log-e2e</artifactId> + + <properties> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <maven.resources.plugin.version>3.3.1</maven.resources.plugin.version> + <maven.dependency.plugin.version>3.6.1</maven.dependency.plugin.version> + + </properties> + + <dependencies> + <!-- HertzBeat modules --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-startup</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-manager</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-log</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-alerter</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-warehouse</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + + <!-- TestContainers --> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers-junit-jupiter</artifactId> + <scope>test</scope> + </dependency> + + <!-- Spring Boot Test --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-test-autoconfigure</artifactId> + <scope>test</scope> + </dependency> + + <!-- JUnit 5 --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <scope>test</scope> + </dependency> + + <!-- AssertJ --> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + + <!-- Awaitility --> + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility</artifactId> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/alert/LogPeriodicAlertE2eTest.java b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/alert/LogPeriodicAlertE2eTest.java new file mode 100644 index 0000000..451f8ce --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/alert/LogPeriodicAlertE2eTest.java @@ -0,0 +1,242 @@ +/* + * 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.alert; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.TestPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.awaitility.Awaitility.await; +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.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.doAnswer; + +/** + * E2E tests for periodic log alert processing. + */ +@SpringBootTest(classes = org.apache.hertzbeat.startup.HertzBeatApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = { + "warehouse.store.duckdb.enabled=false", + "warehouse.store.greptime.enabled=true" +}) +@Slf4j +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class LogPeriodicAlertE2eTest { + + private static final String VECTOR_IMAGE = "timberio/vector:latest-alpine"; + private static final int VECTOR_PORT = 8686; + private static final String VECTOR_CONFIG_PATH = "/etc/vector/vector.yml"; + private static final String ENV_HERTZBEAT_PORT = "HERTZBEAT_PORT"; + private static final String GREPTIME_IMAGE = "greptime/greptimedb:latest"; + private static final int GREPTIME_HTTP_PORT = 4000; + private static final int GREPTIME_GRPC_PORT = 4001; + private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(120); + + @LocalServerPort + private int port; + + @Autowired + PeriodicAlertRuleScheduler periodicAlertRuleScheduler; + + AlertDefine errorCountAlertByGroup; + AlertDefine errorCountAlertByIndividual; + + @MockitoSpyBean + private AlarmCommonReduce alarmCommonReduce; + + static GenericContainer<?> vector; + + static GenericContainer<?> greptimedb; + + static { + greptimedb = new GenericContainer<>(DockerImageName.parse(GREPTIME_IMAGE)) + .withExposedPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT) + .withCommand("standalone", "start", + "--http-addr", "0.0.0.0:" + GREPTIME_HTTP_PORT, + "--rpc-bind-addr", "0.0.0.0:" + GREPTIME_GRPC_PORT) + .waitingFor(Wait.forListeningPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT)) + .withStartupTimeout(CONTAINER_STARTUP_TIMEOUT); + greptimedb.start(); + } + + @DynamicPropertySource + static void greptimeProps(DynamicPropertyRegistry r) { + // Configure GreptimeDB storage endpoints (dynamic ports) + r.add("warehouse.store.greptime.http-endpoint", () -> "http://localhost:" + greptimedb.getMappedPort(GREPTIME_HTTP_PORT)); + r.add("warehouse.store.greptime.grpc-endpoints", () -> "localhost:" + greptimedb.getMappedPort(GREPTIME_GRPC_PORT)); + r.add("warehouse.store.greptime.username", () -> ""); + r.add("warehouse.store.greptime.password", () -> ""); + } + + @BeforeAll + void setUpAll() throws InterruptedException { + // Setup test alert definitions + setupTestAlertDefines(); + Testcontainers.exposeHostPorts(port); + + // Wait for HertzBeat to be fully ready before starting Vector + log.info("Waiting for HertzBeat to be fully ready on port {}...", port); + Thread.sleep(5000); // Give HertzBeat time to fully initialize + + vector = new GenericContainer<>(DockerImageName.parse(VECTOR_IMAGE)) + .withExposedPorts(VECTOR_PORT) + .withCopyFileToContainer(MountableFile.forClasspathResource("vector.yml"), VECTOR_CONFIG_PATH) + .withCommand("--config", "/etc/vector/vector.yml", "--verbose") + .withLogConsumer(outputFrame -> log.info("Vector: {}", outputFrame.getUtf8String())) + .withNetwork(Network.newNetwork()) + .withEnv(ENV_HERTZBEAT_PORT, String.valueOf(port)) + .waitingFor(Wait.forListeningPort()) + .withStartupTimeout(CONTAINER_STARTUP_TIMEOUT); + vector.start(); + } + + @Test + void testPeriodicLogAlertWithIndividualAlert() { + + List<SingleAlert> capturedAlerts = new ArrayList<>(); + + doAnswer(invocation -> { + SingleAlert alert = invocation.getArgument(0); + capturedAlerts.add(alert); + return null; + }).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class)); + + periodicAlertRuleScheduler.updateSchedule(errorCountAlertByIndividual); + + // Wait for periodic individual alert to be generated through AlarmCommonReduce + await().atMost(Duration.ofSeconds(60)) + .pollInterval(Duration.ofSeconds(3)) + .untilAsserted(() -> assertFalse(capturedAlerts.isEmpty(), + "Should have generated at least one periodic individual alert")); + + // Verify alert properties + SingleAlert firstAlert = capturedAlerts.get(0); + assertNotNull(firstAlert, "First periodic alert should not be null"); + assertEquals(CommonConstants.ALERT_STATUS_FIRING, firstAlert.getStatus(), "Alert should be in firing status"); + assertNotNull(firstAlert.getLabels(), "Alert should have labels"); + assertTrue(firstAlert.getLabels().containsKey(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have severity label"); + assertEquals(CommonConstants.ALERT_SEVERITY_CRITICAL, firstAlert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have critical severity"); + } + + @Test + void testPeriodicLogAlertWithGroupAlert() { + + List<List<SingleAlert>> capturedGroupAlerts = new ArrayList<>(); + + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + List<SingleAlert> alerts = invocation.getArgument(1); + capturedGroupAlerts.add(alerts); + return null; + }).when(alarmCommonReduce).reduceAndSendAlarmGroup(anyMap(), anyList()); + + periodicAlertRuleScheduler.updateSchedule(errorCountAlertByGroup); + + await().atMost(Duration.ofSeconds(60)) + .pollInterval(Duration.ofSeconds(3)) + .untilAsserted(() -> { + Optional<SingleAlert> matchedAlert = capturedGroupAlerts.stream() + .flatMap(List::stream) + .filter(alert -> alert.getLabels() != null) + .filter(alert -> String.valueOf(errorCountAlertByGroup.getId()) + .equals(alert.getLabels().get(CommonConstants.LABEL_DEFINE_ID))) + .findFirst(); + + assertTrue(matchedAlert.isPresent(), "Should have captured group alert from target alert define"); + SingleAlert anyAlert = matchedAlert.get(); + assertEquals(CommonConstants.ALERT_STATUS_FIRING, anyAlert.getStatus(), "Alert should be in firing status"); + assertEquals(CommonConstants.ALERT_SEVERITY_CRITICAL, + anyAlert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY), + "Alert should have critical severity"); + assertTrue(anyAlert.getTriggerTimes() >= 1, "Alert should indicate aggregated trigger times"); + }); + } + + /** + * Setup test alert definitions for periodic log processing + */ + private void setupTestAlertDefines() { + // group + errorCountAlertByGroup = AlertDefine.builder() + .id(10L) + .name("periodic_error_count_alert_group") + .type(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC) + .expr("SELECT COUNT(*) as error_count FROM hertzbeat_logs WHERE time_unix_nano > NOW() - INTERVAL '10 minute'") + .period(10) // Faster schedule for tests + .template("High error count detected: {{ error_count }} errors in last period") + .datasource("sql") + .enable(true) + .build(); + + Map<String, String> errorLabelsByGroup = new HashMap<>(); + errorLabelsByGroup.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL); + errorLabelsByGroup.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP); + errorLabelsByGroup.put("type", "error_count"); + errorCountAlertByGroup.setLabels(errorLabelsByGroup); + + // individual + errorCountAlertByIndividual = AlertDefine.builder() + .id(11L) + .name("periodic_error_count_alert_individual") + .type(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC) + .expr("SELECT COUNT(*) as error_count, severity_text FROM hertzbeat_logs " + + "WHERE severity_text = 'ERROR' AND time_unix_nano > NOW() - INTERVAL '5 minute' GROUP BY severity_text HAVING COUNT(*) > 2") + .period(10) // Faster schedule for tests + .template("High error count detected: {{ error_count }} errors in last period") + .datasource("sql") + .enable(true) + .build(); + + Map<String, String> errorLabelsByIndividual = new HashMap<>(); + errorLabelsByIndividual.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL); + errorLabelsByIndividual.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL); + errorLabelsByIndividual.put("type", "error_count"); + errorCountAlertByIndividual.setLabels(errorLabelsByIndividual); + } +} diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/alert/LogRealTimeAlertE2eTest.java b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/alert/LogRealTimeAlertE2eTest.java new file mode 100644 index 0000000..f7f257b --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/alert/LogRealTimeAlertE2eTest.java @@ -0,0 +1,207 @@ +/* + * 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.alert; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.common.cache.CacheFactory; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.SingleAlert; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.awaitility.Awaitility.await; +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.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.doAnswer; + +/** + * E2E tests for real-time log alert processing. + */ +@SpringBootTest(classes = org.apache.hertzbeat.startup.HertzBeatApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Slf4j +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class LogRealTimeAlertE2eTest { + + private static final String VECTOR_IMAGE = "timberio/vector:latest-alpine"; + private static final int VECTOR_PORT = 8686; + private static final String VECTOR_CONFIG_PATH = "/etc/vector/vector.yml"; + private static final String ENV_HERTZBEAT_PORT = "HERTZBEAT_PORT"; + private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(180); + private static final Duration TEST_WAIT_TIMEOUT = Duration.ofSeconds(120); + + @LocalServerPort + private int port; + + private final List<SingleAlert> capturedAlerts = new ArrayList<>(); + private final ArrayList<List<SingleAlert>> capturedGroupAlerts = new ArrayList<>(); + + @MockitoSpyBean + private AlarmCommonReduce alarmCommonReduce; + + static GenericContainer<?> vector; + + @BeforeAll + void setUpAll() throws InterruptedException { + // Setup test alert definitions + setupTestAlertDefines(); + + // Expose host ports for testcontainers + Testcontainers.exposeHostPorts(port); + + // Wait for HertzBeat to be fully ready before starting Vector + log.info("Waiting for HertzBeat to be fully ready on port {}...", port); + Thread.sleep(5000); // Give HertzBeat time to fully initialize + + vector = new GenericContainer<>(DockerImageName.parse(VECTOR_IMAGE)) + .withExposedPorts(VECTOR_PORT) + .withCopyFileToContainer(MountableFile.forClasspathResource("vector.yml"), VECTOR_CONFIG_PATH) + .withCommand("--config", "/etc/vector/vector.yml", "--verbose") + .withLogConsumer(outputFrame -> log.info("Vector: {}", outputFrame.getUtf8String())) + .withNetwork(Network.newNetwork()) + .withEnv(ENV_HERTZBEAT_PORT, String.valueOf(port)) + .waitingFor(Wait.forListeningPort()) + .withStartupTimeout(CONTAINER_STARTUP_TIMEOUT); + vector.start(); + } + + @Test + void testRealTimeLogAlertWithIndividualAlert() { + doAnswer(invocation -> { + SingleAlert alert = invocation.getArgument(0); + capturedAlerts.add(alert); + return null; + }).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class)); + // Clear any existing captured alerts + capturedAlerts.clear(); + + // Wait for real alert to be generated through AlarmCommonReduce + await().atMost(TEST_WAIT_TIMEOUT) + .pollInterval(Duration.ofSeconds(3)) + .untilAsserted(() -> assertFalse(capturedAlerts.isEmpty(), + "Should have generated at least one alert for error logs")); + + // Verify alert properties + SingleAlert firstAlert = capturedAlerts.get(0); + assertNotNull(firstAlert, "First alert should not be null"); + assertEquals(CommonConstants.ALERT_STATUS_FIRING, firstAlert.getStatus(), "Alert should be in firing status"); + assertNotNull(firstAlert.getLabels(), "Alert should have labels"); + assertTrue(firstAlert.getLabels().containsKey(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have severity label"); + assertEquals(CommonConstants.ALERT_SEVERITY_CRITICAL, firstAlert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have critical severity"); + } + + @Test + void testRealTimeLogAlertWithGroupAlert() { + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + List<SingleAlert> alerts = invocation.getArgument(1); + capturedGroupAlerts.add(alerts); + return null; + }).when(alarmCommonReduce).reduceAndSendAlarmGroup(anyMap(), anyList()); + + // Clear any existing captured alerts + capturedGroupAlerts.clear(); + + // Wait for group alert to be generated via AlarmCommonReduce + await().atMost(TEST_WAIT_TIMEOUT) + .pollInterval(Duration.ofSeconds(3)) + .untilAsserted(() -> assertFalse(capturedGroupAlerts.isEmpty(), + "Should have generated high frequency warning group alert")); + + List<SingleAlert> groupAlerts = capturedGroupAlerts.get(0); + assertNotNull(groupAlerts, "Group alerts should not be null"); + assertTrue(groupAlerts.size() >= 2, "Group should contain at least 5 alerts"); + + SingleAlert anyAlert = groupAlerts.get(0); + assertEquals(CommonConstants.ALERT_STATUS_FIRING, anyAlert.getStatus(), "Alert should be in firing status"); + assertNotNull(anyAlert.getLabels(), "Alert should have labels"); + assertEquals(CommonConstants.ALERT_SEVERITY_WARNING, anyAlert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have warning severity"); + assertTrue(anyAlert.getTriggerTimes() >= 2, "Alert should indicate high frequency trigger"); + } + + /** + * Setup test alert definitions for real-time log processing + */ + private void setupTestAlertDefines() { + List<AlertDefine> alertDefines = new ArrayList<>(); + + // Create error log alert definition + AlertDefine errorLogAlert = AlertDefine.builder() + .id(1L) + .name("error_log_alert") + .type(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME) + .expr("log.severityText == 'ERROR'") + .period(10) // 10 seconds window + .times(1) // Trigger on first occurrence + .template("Error detected: {{ body }}") + .enable(true) + .build(); + + Map<String, String> errorLabels = new HashMap<>(); + errorLabels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL); + errorLabels.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL); + errorLogAlert.setLabels(errorLabels); + + alertDefines.add(errorLogAlert); + + // Create high frequency warning alert definition + AlertDefine highFrequencyWarning = AlertDefine.builder() + .id(2L) + .name("high_frequency_warning") + .type(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME) + .expr("log.severityText == 'ERROR'") + .period(10) + .times(2) + .template("High frequency warnings detected: {{ __rows__ }} warnings in 30 seconds") + .enable(true) + .build(); + + Map<String, String> warningLabels = new HashMap<>(); + warningLabels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_WARNING); + warningLabels.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP); + highFrequencyWarning.setLabels(warningLabels); + + alertDefines.add(highFrequencyWarning); + + // Set alert definitions in cache + CacheFactory.setLogAlertDefineCache(alertDefines); + } + +} diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/ingestion/LogIngestionE2eTest.java b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/ingestion/LogIngestionE2eTest.java new file mode 100644 index 0000000..30cab01 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/ingestion/LogIngestionE2eTest.java @@ -0,0 +1,114 @@ +/* + * 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.ingestion; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * E2E tests for log ingestion. + */ +@SpringBootTest(classes = org.apache.hertzbeat.startup.HertzBeatApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Slf4j +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class LogIngestionE2eTest { + + private static final String VECTOR_IMAGE = "timberio/vector:latest-alpine"; + private static final int VECTOR_PORT = 8686; + private static final String VECTOR_CONFIG_PATH = "/etc/vector/vector.yml"; + private static final String ENV_HERTZBEAT_PORT = "HERTZBEAT_PORT"; + private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(120); + + @LocalServerPort + private int port; + + @Autowired + private CommonDataQueue commonDataQueue; + + static GenericContainer<?> vector; + + @BeforeAll + void setUpAll() throws InterruptedException { + Testcontainers.exposeHostPorts(port); + + // Wait for HertzBeat to be fully ready before starting Vector + log.info("Waiting for HertzBeat to be fully ready on port {}...", port); + Thread.sleep(5000); // Give HertzBeat time to fully initialize + + vector = new GenericContainer<>(DockerImageName.parse(VECTOR_IMAGE)) + .withExposedPorts(VECTOR_PORT) + .withCopyFileToContainer(MountableFile.forClasspathResource("vector.yml"), VECTOR_CONFIG_PATH) + .withCommand("--config", "/etc/vector/vector.yml", "--verbose") + .withLogConsumer(outputFrame -> log.info("Vector: {}", outputFrame.getUtf8String())) + .withNetwork(Network.newNetwork()) + .withEnv(ENV_HERTZBEAT_PORT, String.valueOf(port)) + .waitingFor(Wait.forListeningPort()) + .withStartupTimeout(CONTAINER_STARTUP_TIMEOUT); + vector.start(); + } + + @Test + void testLogIngestion() { + + // Start polling for log entries from the queue + List<LogEntry> capturedLogs = new ArrayList<>(); + + // Wait for Vector to generate and send demo logs to HertzBeat + await().atMost(Duration.ofSeconds(60)) + .pollInterval(Duration.ofSeconds(3)) + .untilAsserted(() -> { + try { + LogEntry logEntry = commonDataQueue.pollLogEntry(); + if (logEntry != null) { + capturedLogs.add(logEntry); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Test interrupted", e); + } + }); + + // Verify the captured logs + assertFalse(capturedLogs.isEmpty(), "No logs were captured from Vector"); + LogEntry firstLog = capturedLogs.get(0); + assertNotNull(firstLog, "First log should not be null"); + assertNotNull(firstLog.getBody(), "Log body should not be null"); + assertNotNull(firstLog.getSeverityText(), "Severity text should not be null"); + } +} diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/storage/GreptimeLogStorageE2eTest.java b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/storage/GreptimeLogStorageE2eTest.java new file mode 100644 index 0000000..9a0fbae --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/storage/GreptimeLogStorageE2eTest.java @@ -0,0 +1,187 @@ +/* + * 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.storage; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeDbDataStorage; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.TestPropertySource; + +import java.util.ArrayList; +import java.util.List; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * E2E tests for GreptimeDB log storage. + */ +@SpringBootTest(classes = org.apache.hertzbeat.startup.HertzBeatApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = { + "warehouse.store.duckdb.enabled=false", + "warehouse.store.greptime.enabled=true" +}) +@Slf4j +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class GreptimeLogStorageE2eTest { + + private static final String VECTOR_IMAGE = "timberio/vector:latest-alpine"; + private static final int VECTOR_PORT = 8686; + private static final String VECTOR_CONFIG_PATH = "/etc/vector/vector.yml"; + private static final String ENV_HERTZBEAT_PORT = "HERTZBEAT_PORT"; + private static final String GREPTIME_IMAGE = "greptime/greptimedb:latest"; + private static final int GREPTIME_HTTP_PORT = 4000; + private static final int GREPTIME_GRPC_PORT = 4001; + private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(120); + + @LocalServerPort + private int port; + + @Autowired + private CommonDataQueue commonDataQueue; + + @Autowired + private GreptimeDbDataStorage greptimeDbDataStorage; + + static GenericContainer<?> vector; + static GenericContainer<?> greptimedb; + + static { + greptimedb = new GenericContainer<>(DockerImageName.parse(GREPTIME_IMAGE)) + .withExposedPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT) + .withCommand("standalone", "start", + "--http-addr", "0.0.0.0:" + GREPTIME_HTTP_PORT, + "--rpc-bind-addr", "0.0.0.0:" + GREPTIME_GRPC_PORT) + .waitingFor(Wait.forListeningPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT)) + .withStartupTimeout(CONTAINER_STARTUP_TIMEOUT); + greptimedb.start(); + } + + @DynamicPropertySource + static void greptimeProps(DynamicPropertyRegistry r) { + r.add("warehouse.store.greptime.http-endpoint", () -> "http://localhost:" + greptimedb.getMappedPort(GREPTIME_HTTP_PORT)); + r.add("warehouse.store.greptime.grpc-endpoints", () -> "localhost:" + greptimedb.getMappedPort(GREPTIME_GRPC_PORT)); + r.add("warehouse.store.greptime.username", () -> ""); + r.add("warehouse.store.greptime.password", () -> ""); + } + + + @BeforeAll + void setUpAll() throws InterruptedException { + // Expose host ports for testcontainers + Testcontainers.exposeHostPorts(port); + + // Wait for HertzBeat to be fully ready before starting Vector + log.info("Waiting for HertzBeat to be fully ready on port {}...", port); + Thread.sleep(5000); // Give HertzBeat time to fully initialize + + vector = new GenericContainer<>(DockerImageName.parse(VECTOR_IMAGE)) + .withExposedPorts(VECTOR_PORT) + .withCopyFileToContainer(MountableFile.forClasspathResource("vector.yml"), VECTOR_CONFIG_PATH) + .withCommand("--config", "/etc/vector/vector.yml", "--verbose") + .withLogConsumer(outputFrame -> log.info("Vector: {}", outputFrame.getUtf8String())) + .withNetwork(Network.newNetwork()) + .withEnv(ENV_HERTZBEAT_PORT, String.valueOf(port)) + .waitingFor(Wait.forListeningPort()) + .withStartupTimeout(CONTAINER_STARTUP_TIMEOUT); + vector.start(); + } + + @Test + void testLogStorageToGreptimeDb() { + log.info("GreptimeDbDataStorage serverAvailable: {}", greptimeDbDataStorage.isServerAvailable()); + + List<LogEntry> capturedLogs = new ArrayList<>(); + + // Wait for Vector to generate and send logs to HertzBeat + await().atMost(Duration.ofSeconds(30)) + .pollInterval(Duration.ofSeconds(3)) + .untilAsserted(() -> { + // Poll log entries from the queue (non-blocking) + try { + LogEntry logEntry = commonDataQueue.pollLogEntry(); + if (logEntry != null) { + capturedLogs.add(logEntry); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Test interrupted", e); + } + + // Assert that we have captured at least some logs + assertFalse(capturedLogs.isEmpty(), "Should have captured at least one log entry"); + }); + + // Verify the captured logs + assertFalse(capturedLogs.isEmpty(), "No logs were captured from Vector"); + LogEntry firstLog = capturedLogs.get(0); + assertNotNull(firstLog, "First log should not be null"); + assertNotNull(firstLog.getBody(), "Log body should not be null"); + assertNotNull(firstLog.getSeverityText(), "Severity text should not be null"); + + // Directly write logs to GreptimeDB to test storage functionality + log.info("Directly writing {} captured logs to GreptimeDB", capturedLogs.size()); + greptimeDbDataStorage.saveLogDataBatch(capturedLogs); + + // Give some time for the write to complete + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // Additional wait to ensure logs are persisted to GreptimeDB + await().atMost(Duration.ofSeconds(30)) + .pollInterval(Duration.ofSeconds(2)) + .untilAsserted(() -> { + // Query GreptimeDB directly to verify data persistence + List<LogEntry> storedLogs = queryStoredLogs(); + log.info("Queried {} logs from GreptimeDB", storedLogs.size()); + assertFalse(storedLogs.isEmpty(), "Should have logs stored in GreptimeDB"); + }); + } + + /** + * Helper method to query stored logs directly from GreptimeDB + */ + private List<LogEntry> queryStoredLogs() { + // Query without time condition to verify data exists + List<LogEntry> result = greptimeDbDataStorage.queryLogsByMultipleConditions( + null, null, null, null, null, null, null); + log.info("queryLogsByMultipleConditions returned {} entries", result.size()); + return result; + } +} diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml new file mode 100644 index 0000000..f04c5a2 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml @@ -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. + +## -- sureness.yml account source -- ## + +# config the resource restful api that need auth protection, base rbac +# rule: api===method===role +# eg: /api/v1/source1===get===[admin] means /api/v2/host===post support role[admin] access. +# eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by any role. +resourceRole: + - /api/account/auth/refresh===post===[admin,user,guest] + - /api/apps/**===get===[admin,user,guest] + - /api/monitor/**===get===[admin,user,guest] + - /api/monitor/**===post===[admin,user] + - /api/monitor/**===put===[admin,user] + - /api/monitor/**===delete==[admin] + - /api/monitors/**===get===[admin,user,guest] + - /api/monitors/**===post===[admin,user] + - /api/monitors/**===put===[admin,user] + - /api/monitors/**===delete===[admin] + - /api/alert/**===get===[admin,user,guest] + - /api/alert/**===post===[admin,user] + - /api/alert/**===put===[admin,user] + - /api/alert/**===delete===[admin] + - /api/alerts/**===get===[admin,user,guest] + - /api/alerts/**===post===[admin,user] + - /api/alerts/**===put===[admin,user] + - /api/alerts/**===delete===[admin] + - /api/notice/**===get===[admin,user,guest] + - /api/notice/**===post===[admin,user] + - /api/notice/**===put===[admin,user] + - /api/notice/**===delete===[admin] + - /api/tag/**===get===[admin,user,guest] + - /api/tag/**===post===[admin,user] + - /api/tag/**===put===[admin,user] + - /api/tag/**===delete===[admin] + - /api/summary/**===get===[admin,user,guest] + - /api/summary/**===post===[admin,user] + - /api/summary/**===put===[admin,user] + - /api/summary/**===delete===[admin] + - /api/collector/**===get===[admin,user,guest] + - /api/collector/**===post===[admin,user] + - /api/collector/**===put===[admin,user] + - /api/collector/**===delete===[admin] + - /api/status/page/**===get===[admin,user,guest] + - /api/status/page/**===post===[admin,user] + - /api/status/page/**===put===[admin,user] + - /api/status/page/**===delete===[admin] + - /api/grafana/**===get===[admin,user,guest] + - /api/grafana/**===post===[admin,user] + - /api/grafana/**===put===[admin,user] + - /api/grafana/**===delete===[admin] + - /api/bulletin/**===get===[admin,user,guest] + - /api/bulletin/**===post===[admin,user] + - /api/bulletin/**===put===[admin,user] + - /api/bulletin/**===delete===[admin] + - /api/sse/**===get===[admin,user] + - /api/sse/**===post===[admin,user] + - /api/chat/**===get===[admin,user] + - /api/chat/**===post===[admin,user] + - /api/logs/ingest/**===post===[admin,user] + +# config the resource restful api that need bypass auth protection +# rule: api===method +# 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/push/**===* + - /api/status/page/public/**===* + - /api/manager/sse/**===* + # web ui resource + - /===get + - /assets/**===get + - /dashboard/**===get + - /monitors/**===get + - /alert/**===get + - /account/**===get + - /setting/**===get + - /passport/**===get + - /status/**===get + - /**/*.html===get + - /**/*.js===get + - /**/*.css===get + - /**/*.ico===get + - /**/*.ttf===get + - /**/*.png===get + - /**/*.gif===get + - /**/*.jpg===get + - /**/*.svg===get + - /**/*.json===get + - /**/*.woff===get + - /**/*.eot===get + # swagger ui resource + - /swagger-resources/**===get + - /v2/api-docs===get + - /v3/api-docs===get + # h2 database + - /h2-console/**===* + +# account info config +# eg: admin has role [admin,user], password is hertzbeat +# eg: tom has role [user], password is hertzbeat +# eg: lili has role [guest], plain password is lili, salt is 123, salted password is 1A676730B0C7F54654B0E09184448289 +account: + - appId: admin + credential: hertzbeat + role: [admin] + - appId: tom + credential: hertzbeat + role: [user] + - appId: guest + credential: hertzbeat + role: [guest] + - appId: lili + # credential = MD5(password + salt) + # plain password: hertzbeat + # attention: digest authentication does not support salted encrypted password accounts + credential: 94C6B34E7A199A9F9D4E1F208093B489 + salt: 123 + role: [user] diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/vector.yml b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/vector.yml new file mode 100644 index 0000000..f86f2b0 --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/vector.yml @@ -0,0 +1,126 @@ +# 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. + +# Vector configuration for HertzBeat E2E testing + +api: + enabled: true + address: "0.0.0.0:8686" + +sources: + generate_syslog: + type: "demo_logs" + format: "syslog" + count: 100000 + interval: 1 + +transforms: + remap_syslog: + inputs: ["generate_syslog"] + type: "remap" + source: | + syslog = parse_syslog!(.message) + + severity_text = if includes(["emerg", "err", "crit", "alert"], syslog.severity) { + "ERROR" + } else if syslog.severity == "warning" { + "WARN" + } else if syslog.severity == "debug" { + "DEBUG" + } else if includes(["info", "notice"], syslog.severity) { + "INFO" + } else { + syslog.severity + } + + severity_number = if includes(["emerg", "err", "crit", "alert"], syslog.severity) { + 17 + } else if syslog.severity == "warning" { + 13 + } else if syslog.severity == "debug" { + 5 + } else if includes(["info", "notice"], syslog.severity) { + 9 + } else { + 9 + } + + .resourceLogs = [{ + "resource": { + "attributes": [ + { "key": "source_type", "value": { "stringValue": .source_type } }, + { "key": "service.name", "value": { "stringValue": syslog.appname } }, + { "key": "host.hostname", "value": { "stringValue": syslog.hostname } } + ] + }, + "scopeLogs": [{ + "scope": { + "name": syslog.msgid + }, + "logRecords": [{ + "timeUnixNano": to_unix_timestamp!(syslog.timestamp, unit: "nanoseconds"), + "body": { "stringValue": syslog.message }, + "severityText": severity_text, + "severityNumber": severity_number, + "attributes": [ + { "key": "syslog.procid", "value": { "stringValue": to_string(syslog.procid) } }, + { "key": "syslog.facility", "value": { "stringValue": syslog.facility } }, + { "key": "syslog.version", "value": { "stringValue": to_string(syslog.version) } } + ] + }] + }] + }] + + del(.message) + del(.timestamp) + del(.service) + del(.source_type) + +sinks: + # Debug sink to log all processed data + debug_console: + inputs: [ "remap_syslog" ] + type: console + encoding: + codec: json + + # Send to HertzBeat with increased timeout and retry + emit_syslog: + inputs: [ "remap_syslog" ] + type: opentelemetry + protocol: + type: http + uri: "http://host.testcontainers.internal:${HERTZBEAT_PORT:-1157}/api/logs/ingest/otlp" + method: post + encoding: + codec: json + framing: + method: newline_delimited + auth: + strategy: basic + user: admin + password: hertzbeat + # Increase timeout and retry settings for stability in CI environments + request: + headers: + content-type: application/json + timeout_secs: 60 + retry_attempts: 10 + retry_initial_backoff_secs: 2 + retry_max_duration_secs: 120 + # Batch settings for better throughput + batch: + max_bytes: 524288 + timeout_secs: 1 diff --git a/hertzbeat-e2e/pom.xml b/hertzbeat-e2e/pom.xml new file mode 100644 index 0000000..6249cf0 --- /dev/null +++ b/hertzbeat-e2e/pom.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-e2e</artifactId> + <packaging>pom</packaging> + <modules> + <module>hertzbeat-collector-common-e2e</module> + <module>hertzbeat-collector-kafka-e2e</module> + <module>hertzbeat-collector-basic-e2e</module> + <module>hertzbeat-collector-mysql-r2dbc-e2e</module> + <module>hertzbeat-log-e2e</module> + </modules> + + <properties> + <maven.test.skip>true</maven.test.skip> + <maven.compiler.source>${java.version}</maven.compiler.source> + <maven.compiler.target>${java.version}</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <testcontainers.version>2.0.3</testcontainers.version> + <junit.version>4.13.2</junit.version> + <awaitility.version>4.2.0</awaitility.version> + </properties> + + + <dependencies> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers</artifactId> + <version>${testcontainers.version}</version> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>${junit.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility</artifactId> + <version>${awaitility.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-startup</artifactId> + <version>${hertzbeat.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers-bom</artifactId> + <version>${testcontainers.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + +</project> diff --git a/hertzbeat-grafana/pom.xml b/hertzbeat-grafana/pom.xml new file mode 100644 index 0000000..97b4296 --- /dev/null +++ b/hertzbeat-grafana/pom.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + + <artifactId>hertzbeat-grafana</artifactId> + <name>${project.artifactId}</name> + <properties> + <maven-jar-plugin.version>3.2.0</maven-jar-plugin.version> + <maven-assembly-plugin.version>3.3.0</maven-assembly-plugin.version> + </properties> + + <dependencies> + <!-- hertzbeat base --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-base</artifactId> + </dependency> + <!-- hertzbeat warehouse --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-warehouse</artifactId> + </dependency> + <!-- hertzbeat common-core --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-common-core</artifactId> + </dependency> + <!-- jpa --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + </dependency> + <!-- swagger --> + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + </dependency> + </dependencies> +</project> diff --git a/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/common/GrafanaConstants.java b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/common/GrafanaConstants.java new file mode 100644 index 0000000..e2dc9d2 --- /dev/null +++ b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/common/GrafanaConstants.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.grafana.common; + +import org.apache.hertzbeat.common.constants.ConfigConstants; +import java.net.URL; + +/** + * Grafana Common Constants + */ +public interface GrafanaConstants { + + String ADMIN = "admin"; + + String KIOSK = "?kiosk=tv"; + + String REFRESH = "&refresh=15s"; + + String INSTANCE = "&var-instance="; + + String THEME = "&theme="; + + String CREATE_DASHBOARD_API = "/api/dashboards/db"; + + String DELETE_DASHBOARD_API = "/api/dashboards/uid/%s"; + + String DATASOURCE_BASE_NAME = "hertzbeat"; + + String DATASOURCE_TYPE = "prometheus"; + + String DATASOURCE_ACCESS = "proxy"; + + String CREATE_DATASOURCE_API = "/api/datasources"; + + String QUERY_DATASOURCE_API = "/api/datasources/name/"; + + String GET_SERVICE_ACCOUNTS_API = "%s/api/serviceaccounts/search"; + + String ACCOUNT_NAME = ConfigConstants.SystemConstant.PROJECT_NAME; + + String ACCOUNT_ROLE = "Admin"; + + String CREATE_SERVICE_ACCOUNT_API = "%s/api/serviceaccounts"; + + String CREATE_SERVICE_TOKEN_API = "%s/api/serviceaccounts/%d/tokens"; + + String GRAFANA_CONFIG = "grafanaConfig"; + + /** + * Generate data source name based on type and URL + * @param type Data source type (vm or greptime) + * @param url Data source URL + * @return Generated unique data source name + */ + static String generateDatasourceName(String type, String url) { + try { + URL parsedUrl = new URL(url); + String host = parsedUrl.getHost(); + int port = parsedUrl.getPort(); + + // Extract host without subdomain for cleaner names + String hostPart = host != null ? host.replaceAll("^www\\.", "") : "localhost"; + + // Include port if it's not default + String portPart = (port > 0 && port != 80 && port != 443) ? "-" + port : ""; + + return String.format("%s-%s-%s%s", DATASOURCE_BASE_NAME, type, hostPart, portPart); + } catch (Exception e) { + // Fallback to simple naming if URL parsing fails + return String.format("%s-%s-%s", DATASOURCE_BASE_NAME, type, url.hashCode()); + } + } + + /** + * Generate USE_DATASOURCE parameter with dynamic datasource name + * @param datasourceName The datasource name + * @return USE_DATASOURCE parameter string + */ + static String generateUseDatasource(String datasourceName) { + return "&var-ds=" + datasourceName; + } +} diff --git a/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaAutoConfiguration.java b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaAutoConfiguration.java new file mode 100644 index 0000000..7360536 --- /dev/null +++ b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaAutoConfiguration.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.grafana.config; + +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; + +/** + * Grafana auto configuration. + */ +@ComponentScan(basePackages = ConfigConstants.PkgConstant.PKG + + SignConstants.DOT + + ConfigConstants.FunctionModuleConstants.GRAFANA +) +@EnableConfigurationProperties(GrafanaProperties.class) +public class GrafanaAutoConfiguration { +} diff --git a/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaInit.java b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaInit.java new file mode 100644 index 0000000..8e85a67 --- /dev/null +++ b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaInit.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.grafana.config; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.grafana.service.DatasourceService; +import org.apache.hertzbeat.grafana.service.ServiceAccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +/** + * grafana init + */ +@Component +@Slf4j +public class GrafanaInit implements CommandLineRunner { + @Autowired + private GrafanaProperties grafanaProperties; + @Autowired + private ServiceAccountService serviceAccountService; + @Autowired + private DatasourceService datasourceService; + + //1. Determine whether the configuration is filled out completely + //2. Determine whether there is an account, if not, create and ensure that the account is unique + //3. Determine whether there is a token, if not, create and ensure that the account is unique. + @Override + public void run(String... args) throws Exception { + if (grafanaProperties.enabled()) { + log.info("grafana init start"); + try { + String token = serviceAccountService.applyForToken(); + datasourceService.existOrCreateDatasource(token); + } catch (Exception e) { + log.error("grafana init error", e); + } + } + } + + +} diff --git a/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaProperties.java b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaProperties.java new file mode 100644 index 0000000..74144b7 --- /dev/null +++ b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaProperties.java @@ -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.grafana.config; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.grafana.common.GrafanaConstants; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.DefaultValue; + +/** + * grafana configuration + */ +@Slf4j +@ConfigurationProperties(prefix = ConfigConstants.FunctionModuleConstants.GRAFANA) +public record GrafanaProperties(@DefaultValue("false") boolean enabled, + @DefaultValue("http://127.0.0.1:3000") String url, + @DefaultValue("http://127.0.0.1:3000") String exposeUrl, + @DefaultValue(GrafanaConstants.ADMIN) String username, + @DefaultValue(GrafanaConstants.ADMIN) String password) { + /** + * get the prefix of the grafana url, such as http or https + */ + public String getPrefix() { + if (url.startsWith(NetworkConstants.HTTP_HEADER)) { + return NetworkConstants.HTTP_HEADER; + } else if (url.startsWith(NetworkConstants.HTTPS_HEADER)) { + return NetworkConstants.HTTPS_HEADER; + } + return NetworkConstants.HTTP_HEADER; + } + + /** + * get the grafana url without the prefix, such as localhost:3000 + */ + public String getUrl() { + if (getPrefix().equals(NetworkConstants.HTTP_HEADER)) { + return url.replace(NetworkConstants.HTTP_HEADER, ""); + } else if (getPrefix().equals(NetworkConstants.HTTPS_HEADER)) { + return url.replace(NetworkConstants.HTTPS_HEADER, ""); + } + return url; + } +} diff --git a/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/controller/DashboardController.java b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/controller/DashboardController.java new file mode 100644 index 0000000..b81ed9d --- /dev/null +++ b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/controller/DashboardController.java @@ -0,0 +1,107 @@ +/* + * 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.grafana.controller; + +import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard; +import org.apache.hertzbeat.grafana.service.DashboardService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Controller for managing Grafana dashboards via API. + */ +@Slf4j +@Tag(name = "Dashboard API") +@RestController +@RequestMapping(path = "/api/grafana/dashboard", + produces = {APPLICATION_JSON_VALUE}) +public class DashboardController { + + @Autowired + private DashboardService dashboardService; + + /** + * Creates a new Grafana dashboard. + * + * @param dashboardJson JSON representation of the dashboard + * @param monitorId the ID of the monitor associated with the dashboard + * @return ResponseEntity containing success or failure message + */ + @Operation(summary = "Create dashboard", description = "Create dashboard") + @PostMapping + public ResponseEntity<Message<?>> createDashboard(@RequestParam String dashboardJson, @RequestParam Long monitorId) { + try { + ResponseEntity<?> response = dashboardService.createOrUpdateDashboard(dashboardJson, monitorId); + if (response.getStatusCode().is2xxSuccessful()) { + return ResponseEntity.ok(Message.success("create dashboard success")); + } + } catch (Exception e) { + log.error("create dashboard error", e); + return ResponseEntity.ok(Message.fail(FAIL_CODE, e.getMessage())); + } + return ResponseEntity.ok(Message.fail(FAIL_CODE, "create dashboard fail")); + } + + /** + * Retrieves a Grafana dashboard by monitor ID. + * + * @param monitorId the ID of the monitor associated with the dashboard + * @return ResponseEntity containing the GrafanaDashboard object or failure message + */ + @Operation(summary = "Get dashboard by monitor id", description = "Get dashboard by monitor id") + @GetMapping + public ResponseEntity<Message<?>> getDashboardByMonitorId(@RequestParam Long monitorId) { + try { + GrafanaDashboard grafanaDashboard = dashboardService.getDashboardByMonitorId(monitorId); + return ResponseEntity.ok(Message.success(grafanaDashboard)); + } catch (Exception e) { + log.error("get dashboard error", e); + return ResponseEntity.ok(Message.fail(FAIL_CODE, "get dashboard fail")); + } + } + + /** + * Deletes a Grafana dashboard by monitor ID. + * + * @param monitorId the ID of the monitor associated with the dashboard + * @return ResponseEntity containing success or failure message + */ + @Operation(summary = "Delete dashboard by monitor id", description = "Delete dashboard by monitor id") + @DeleteMapping + public ResponseEntity<Message<String>> deleteDashboardByMonitorId(@RequestParam Long monitorId) { + try { + dashboardService.deleteDashboard(monitorId); + } catch (Exception e) { + log.error("delete dashboard error", e); + return ResponseEntity.ok(Message.fail(FAIL_CODE, "delete dashboard fail")); + } + return ResponseEntity.ok(Message.success("delete dashboard success")); + } +} diff --git a/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/dao/DashboardDao.java b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/dao/DashboardDao.java new file mode 100644 index 0000000..126cab7 --- /dev/null +++ b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/dao/DashboardDao.java @@ -0,0 +1,48 @@ +/* + * 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.grafana.dao; + +import java.util.List; +import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * Dashboard Dao + */ +public interface DashboardDao extends JpaRepository<GrafanaDashboard, Long>, JpaSpecificationExecutor<GrafanaDashboard> { + /** + * find by monitor id + * @param monitorId monitor id + * @return dashboard + */ + GrafanaDashboard findByMonitorId(Long monitorId); + + /** + * delete by monitor id + * @param monitorId monitor id + */ + void deleteByMonitorId(Long monitorId); + + /** + * find by dashboard uid + * @param dashboardUid dashboard uid + * @return dashboard + */ + List<GrafanaDashboard> findByUid(String dashboardUid); +} diff --git a/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/dao/GrafanaConfigDao.java b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/dao/GrafanaConfigDao.java new file mode 100644 index 0000000..1a74bb0 --- /dev/null +++ b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/dao/GrafanaConfigDao.java @@ -0,0 +1,36 @@ +/* + * 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.grafana.dao; + +import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * Public server configuration Dao + * <p>This interface inherits the two interfaces JpaRepository and JpaSpecificationExecutor, providing basic CRUD operations and specification query capabilities.</p> + */ +public interface GrafanaConfigDao extends JpaRepository<GeneralConfig, Long>, JpaSpecificationExecutor<GeneralConfig> { + + /** + * Query by type + * @param type type + * @return Return the queried configuration information + */ + GeneralConfig findByType(String type); +} diff --git a/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/dto/GrafanaConfig.java b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/dto/GrafanaConfig.java new file mode 100644 index 0000000..3379976 --- /dev/null +++ b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/dto/GrafanaConfig.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.grafana.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * System Configuration + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GrafanaConfig { + + /** + * api token + */ + private String token; +} diff --git a/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/service/DashboardService.java b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/service/DashboardService.java new file mode 100644 index 0000000..95642e6 --- /dev/null +++ b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/service/DashboardService.java @@ -0,0 +1,290 @@ +/* + * 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.grafana.service; + +import tools.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum; +import org.apache.hertzbeat.common.constants.ThemeEnum; +import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard; +import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.grafana.common.GrafanaConstants; +import org.apache.hertzbeat.grafana.config.GrafanaProperties; +import org.apache.hertzbeat.grafana.dao.DashboardDao; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.CREATE_DASHBOARD_API; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.DELETE_DASHBOARD_API; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.INSTANCE; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.KIOSK; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.REFRESH; + +/** + * Service for managing Grafana dashboards. + */ +@Service +@Slf4j +public class DashboardService { + + @Autowired + private ServiceAccountService serviceAccountService; + + @Autowired + private DashboardDao dashboardDao; + + @Autowired + private GrafanaProperties grafanaProperties; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private DatasourceService datasourceService; + + @Autowired + private GeneralConfigDao generalConfigDao; + + /** + * Creates or updates a dashboard in Grafana. + * The "id" field will be removed from the dashboard JSON before sending + * to Grafana to ensure new dashboards are created correctly. + * + * @param dashboardJson the JSON representation of the dashboard definition + * @param monitorId the ID of the monitor associated with the dashboard + * @return ResponseEntity containing the response from Grafana + */ + @Transactional(rollbackFor = Exception.class) + public ResponseEntity<?> createOrUpdateDashboard(String dashboardJson, Long monitorId) { + if (!grafanaProperties.enabled()) { + log.info("HertzBeat Grafana config not enabled"); + throw new RuntimeException("HertzBeat Grafana config not enabled"); + } + + String token = serviceAccountService.getToken(); + String url = grafanaProperties.getPrefix() + grafanaProperties.getUrl() + CREATE_DASHBOARD_API; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(token); + + Map<String, Object> dashboardObjectMap; + try { + dashboardObjectMap = JsonUtil.fromJson(dashboardJson, Map.class); + } catch (Exception e) { + log.error("Failed to parse dashboardJson. Monitor ID: {}. JSON: {}", monitorId, dashboardJson, e); + throw new RuntimeException("Invalid dashboard JSON structure", e); + } + + if (dashboardObjectMap == null) { + log.error("Parsed dashboardJson is null. Monitor ID: {}. Original JSON: {}", monitorId, dashboardJson); + throw new RuntimeException("Parsed dashboard JSON is null"); + } + + if (dashboardObjectMap.containsKey("id")) { + dashboardObjectMap.remove("id"); + log.debug("Removed 'id' field from dashboard JSON for monitorId: {}", monitorId); + } + + // Construct the full request payload for Grafana API + Map<String, Object> requestPayload = new HashMap<>(); + requestPayload.put("dashboard", dashboardObjectMap); + requestPayload.put("overwrite", true); // Overwrite if a dashboard with the same UID exists + + String finalJsonPayload = JsonUtil.toJson(requestPayload); + HttpEntity<String> requestEntity = new HttpEntity<>(finalJsonPayload, headers); + + try { + ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + GrafanaDashboard grafanaDashboard = JsonUtil.fromJson(response.getBody(), GrafanaDashboard.class); + if (grafanaDashboard != null) { + grafanaDashboard.setEnabled(true); + + String currentDatasourceName = datasourceService.getCurrentDatasourceName(); + String useDatasource = currentDatasourceName != null + ? GrafanaConstants.generateUseDatasource(currentDatasourceName) : ""; + + String relativeDashboardUrl = grafanaDashboard.getUrl(); + String fullDashboardUrl = grafanaProperties.exposeUrl().replaceAll("/$", "") + relativeDashboardUrl; + + grafanaDashboard.setUrl(fullDashboardUrl + KIOSK + REFRESH + INSTANCE + monitorId + useDatasource); + + grafanaDashboard.setMonitorId(monitorId); + dashboardDao.save(grafanaDashboard); + log.info("Successfully created/updated Grafana dashboard for monitorId: {}. Response: {}", monitorId, response.getBody()); + } else { + log.error("Failed to parse Grafana response into GrafanaDashboard object. MonitorId: {}. Response body: {}", monitorId, response.getBody()); + } + return response; + } else { + log.error("Failed to create/update Grafana dashboard for monitorId: {}. Status: {}, Response: {}", + monitorId, response.getStatusCode(), response.getBody()); + throw new RuntimeException("Failed to create/update Grafana dashboard: " + response.getStatusCode() + " - " + response.getBody()); + } + } catch (HttpClientErrorException ex) { + String responseBody = ex.getResponseBodyAsString(); + log.error("Grafana API request failed for monitorId: {}. Status: {}. URL: {}. Request: {}. Response: {}", + monitorId, ex.getStatusCode(), url, finalJsonPayload, responseBody, ex); + if (ex instanceof HttpClientErrorException.Forbidden) { + throw new RuntimeException("Grafana Access Denied: " + responseBody, ex); + } else if (ex instanceof HttpClientErrorException.NotFound) { + throw new RuntimeException("Grafana API endpoint or resource not found: " + responseBody, ex); + } + throw new RuntimeException("Grafana API client error (" + ex.getStatusCode() + "): " + responseBody, ex); + } catch (Exception ex) { + log.error("An unexpected error occurred while creating/updating Grafana dashboard for monitorId: {}. URL: {}. Request: {}", + monitorId, url, finalJsonPayload, ex); + throw new RuntimeException("Error during Grafana dashboard operation: " + ex.getMessage(), ex); + } + } + + /** + * Deletes a dashboard in Grafana by monitor ID. + * + * @param monitorId the ID of the monitor associated with the dashboard + */ + @Transactional(rollbackFor = Exception.class) + public void deleteDashboard(Long monitorId) { + GrafanaDashboard grafanaDashboard = dashboardDao.findByMonitorId(monitorId); + if (Objects.isNull(grafanaDashboard)) { + log.info("No Grafana dashboard found for monitorId {} to delete.", monitorId); + return; + } + String uid = grafanaDashboard.getUid(); + List<GrafanaDashboard> grafanaDashboards = dashboardDao.findByUid(uid); + + if (grafanaDashboards.size() > 1) { + dashboardDao.deleteByMonitorId(monitorId); + log.info("Deleted hertzbeat dashboard record for monitorId: {}, Grafana dashboard with UID: {} still used by other monitors.", monitorId, uid); + } else { + String token = serviceAccountService.getToken(); + String url = grafanaProperties.getPrefix() + grafanaProperties.getUrl() + String.format(DELETE_DASHBOARD_API, uid); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(token); + + HttpEntity<Void> requestEntity = new HttpEntity<>(headers); + + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.DELETE, requestEntity, String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + // Delete from local DB only after successful Grafana deletion + dashboardDao.deleteByMonitorId(monitorId); + log.info("Successfully deleted Grafana dashboard with UID: {} and corresponding hertzbeat record for monitorId: {}", uid, monitorId); + } else { + log.error("Failed to delete Grafana dashboard with UID: {}. Status: {}, Response: {}", + uid, response.getStatusCode(), response.getBody()); + throw new RuntimeException("Failed to delete Grafana dashboard: " + response.getStatusCode() + " - " + response.getBody()); + } + } catch (HttpClientErrorException ex) { + String responseBody = ex.getResponseBodyAsString(); + log.error("Grafana API request failed during dashboard deletion for UID: {}. Status: {}. URL: {}. Response: {}", + uid, ex.getStatusCode(), url, responseBody, ex); + if (ex.getStatusCode() == org.springframework.http.HttpStatus.NOT_FOUND) { + log.warn("Grafana dashboard with UID: {} not found during deletion attempt. Assuming already deleted. Deleting local record for monitorId: {}", uid, monitorId); + dashboardDao.deleteByMonitorId(monitorId); + } else { + throw new RuntimeException("Grafana API client error during deletion (" + ex.getStatusCode() + "): " + responseBody, ex); + } + } catch (Exception ex) { + log.error("An unexpected error occurred while deleting Grafana dashboard with UID: {}. URL: {}", uid, url, ex); + throw new RuntimeException("Error during Grafana dashboard deletion: " + ex.getMessage(), ex); + } + } + } + + /** + * Retrieves a dashboard by monitor ID. + * + * @param monitorId the ID of the monitor associated with the dashboard + * @return GrafanaDashboard object + */ + public GrafanaDashboard getDashboardByMonitorId(Long monitorId) { + GrafanaDashboard dashboard = dashboardDao.findByMonitorId(monitorId); + if (null != dashboard + && StringUtils.isNotBlank(dashboard.getUrl()) + && !dashboard.getUrl().contains(GrafanaConstants.THEME)) { + String theme = loadThemeConfig(); + return dashboard.toBuilder() + .url(dashboard.getUrl() + GrafanaConstants.THEME + theme) + .build(); + } + return dashboard; + } + + /** + * Disables a Grafana dashboard by monitor ID. + * (This only updates the local HertzBeat database flag, does not interact with Grafana API) + * + * @param monitorId the ID of the monitor associated with the dashboard + */ + public void closeGrafanaDashboard(Long monitorId) { + GrafanaDashboard grafanaDashboard = dashboardDao.findByMonitorId(monitorId); + if (grafanaDashboard != null) { + if (grafanaDashboard.isEnabled()) { // Only save if there's a change + grafanaDashboard.setEnabled(false); + dashboardDao.save(grafanaDashboard); + log.info("Disabled Grafana dashboard link in HertzBeat for monitorId: {}", monitorId); + } else { + log.info("Grafana dashboard link for monitorId: {} was already disabled.", monitorId); + } + } else { + log.warn("No Grafana dashboard record found for monitorId {} to disable.", monitorId); + } + } + + private String loadThemeConfig() { + try { + GeneralConfig config = generalConfigDao.findByType(GeneralConfigTypeEnum.system.name()); + if (config != null && config.getContent() != null) { + JsonNode root = JsonUtil.fromJson(config.getContent()); + if (root == null) { + return ThemeEnum.LIGHT.getValue(); + } + JsonNode node = root.get("theme"); + if (node != null && !node.isNull()) { + return ThemeEnum.convert(node.asText()); + } + return ThemeEnum.LIGHT.getValue(); + } + } catch (Exception e) { + log.error("Failed to load database theme configuration", e); + } + return ThemeEnum.LIGHT.getValue(); + } +} diff --git a/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/service/DatasourceService.java b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/service/DatasourceService.java new file mode 100644 index 0000000..9828422 --- /dev/null +++ b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/service/DatasourceService.java @@ -0,0 +1,213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.grafana.service; + +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.CREATE_DATASOURCE_API; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.DATASOURCE_ACCESS; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.DATASOURCE_TYPE; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.QUERY_DATASOURCE_API; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.generateDatasourceName; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.grafana.config.GrafanaProperties; +import org.apache.hertzbeat.warehouse.store.history.tsdb.vm.VictoriaMetricsProperties; +import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +/** + * Service for managing Grafana datasources. + * This service checks if a datasource exists and creates it if not. + */ +@Service +@Slf4j +public class DatasourceService { + + private final GrafanaProperties grafanaProperties; + private final VictoriaMetricsProperties warehouseProperties; + private final GreptimeProperties greptimeProperties; + private final RestTemplate restTemplate; + + @Autowired + public DatasourceService( + GrafanaProperties grafanaProperties, + VictoriaMetricsProperties warehouseProperties, + GreptimeProperties greptimeProperties, + RestTemplate restTemplate + ) { + this.grafanaProperties = grafanaProperties; + this.warehouseProperties = warehouseProperties; + this.greptimeProperties = greptimeProperties; + this.restTemplate = restTemplate; + } + + public void existOrCreateDatasource(String token) { + boolean vmEnabled = warehouseProperties.enabled(); + boolean greptimeEnabled = greptimeProperties.enabled(); + + if (vmEnabled && greptimeEnabled) { + throw new IllegalStateException("Conflict: Both VictoriaMetrics and Greptime are enabled, only one can be used for Grafana datasource"); + } + + if (!vmEnabled && !greptimeEnabled) { + log.info("HertzBeat warehouse config not enabled"); + return; + } + + // Determine datasource type and URL + String datasourceType; + String datasourceUrl; + + if (vmEnabled) { + datasourceType = "vm"; + datasourceUrl = warehouseProperties.url(); + } else { + datasourceType = "greptime"; + datasourceUrl = greptimeProperties.httpEndpoint(); + } + + // Generate unique datasource name + String datasourceName = generateDatasourceName(datasourceType, datasourceUrl); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(token); + + String queryUrl = grafanaProperties.getPrefix() + grafanaProperties.getUrl() + QUERY_DATASOURCE_API + datasourceName; + HttpEntity<Void> entity = new HttpEntity<>(headers); + + try { + ResponseEntity<String> response = restTemplate.exchange(queryUrl, HttpMethod.GET, entity, String.class); + if (response.getStatusCode().is2xxSuccessful()) { + log.info("{} datasource already exists", datasourceName); + return; + } + } catch (HttpClientErrorException.NotFound notFound) { + log.info("Datasource {} not found, creating new one", datasourceName); + } catch (Exception e) { + log.error("Query datasource error", e); + return; + } + + // Create new datasource + createDatasource(token, datasourceName, datasourceUrl, datasourceType); + } + + public void createDatasource(String token, String datasourceName, String datasourceUrl, String datasourceType) { + String createUrl = grafanaProperties.getPrefix() + grafanaProperties.getUrl() + CREATE_DATASOURCE_API; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(token); + + DatasourceRequest datasourceRequest; + if ("greptime".equals(datasourceType)) { + datasourceUrl += "/v1/prometheus"; + Map<String, Object> jsonData = new HashMap<>(); + Map<String, Object> secureJsonData = new HashMap<>(); + jsonData.put("httpHeaderName1", "x-greptime-db-name"); + secureJsonData.put("httpHeaderValue1", greptimeProperties.database()); + datasourceRequest = new DatasourceRequest( + datasourceName, + DATASOURCE_TYPE, + DATASOURCE_ACCESS, + datasourceUrl, + false, + jsonData, + secureJsonData + ); + } else { + datasourceRequest = new DatasourceRequest( + datasourceName, + DATASOURCE_TYPE, + DATASOURCE_ACCESS, + datasourceUrl, + false, + null, + null + ); + } + + try { + String body = JsonUtil.toJson(datasourceRequest); + HttpEntity<String> createEntity = new HttpEntity<>(body, headers); + + ResponseEntity<String> createResponse = restTemplate.postForEntity(createUrl, createEntity, String.class); + if (createResponse.getStatusCode().is2xxSuccessful()) { + log.info("Create datasource success"); + } + } catch (HttpClientErrorException.Conflict conflict) { + log.info("Datasource already exists"); + } catch (Exception e) { + log.error("Create datasource error", e); + } + } + + /** + * Request object for creating a Grafana datasource. + * Fields are annotated with @JsonInclude to exclude null values from serialization. + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class DatasourceRequest { + public String name; + public String type; + public String access; + public String url; + public boolean basicAuth; + public Map<String, Object> jsonData; + public Map<String, Object> secureJsonData; + + public DatasourceRequest(String name, String type, String access, String url, boolean basicAuth, Map<String, Object> jsonData, Map<String, Object> secureJsonData) { + this.name = name; + this.type = type; + this.access = access; + this.url = url; + this.basicAuth = basicAuth; + this.jsonData = jsonData; + this.secureJsonData = secureJsonData; + } + } + + /** + * Get the current active datasource name + * @return Current datasource name or null if none active + */ + public String getCurrentDatasourceName() { + boolean vmEnabled = warehouseProperties.enabled(); + boolean greptimeEnabled = greptimeProperties.enabled(); + + if (vmEnabled) { + return generateDatasourceName("vm", warehouseProperties.url()); + } else if (greptimeEnabled) { + return generateDatasourceName("greptime", greptimeProperties.httpEndpoint()); + } + + return null; + } +} \ No newline at end of file diff --git a/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/service/ServiceAccountService.java b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/service/ServiceAccountService.java new file mode 100644 index 0000000..7e1e251 --- /dev/null +++ b/hertzbeat-grafana/src/main/java/org/apache/hertzbeat/grafana/service/ServiceAccountService.java @@ -0,0 +1,206 @@ +/* + * 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.grafana.service; + +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.ACCOUNT_NAME; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.ACCOUNT_ROLE; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.CREATE_SERVICE_ACCOUNT_API; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.CREATE_SERVICE_TOKEN_API; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.GET_SERVICE_ACCOUNTS_API; +import static org.apache.hertzbeat.grafana.common.GrafanaConstants.GRAFANA_CONFIG; +import tools.jackson.databind.JsonNode; +import jakarta.annotation.PostConstruct; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.grafana.config.GrafanaProperties; +import org.apache.hertzbeat.grafana.dao.GrafanaConfigDao; +import org.apache.hertzbeat.grafana.dto.GrafanaConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +/** + * Service for managing Grafana service accounts and tokens. + */ +@Service +@Slf4j +public class ServiceAccountService { + + private final GrafanaProperties grafanaProperties; + private final GrafanaConfigDao grafanaConfigDao; + private final RestTemplate restTemplate; + + private String url; + private String username; + private String password; + private String prefix; + + @Autowired + public ServiceAccountService( + GrafanaProperties grafanaProperties, + GrafanaConfigDao grafanaConfigDao, + RestTemplate restTemplate + ) { + this.grafanaProperties = grafanaProperties; + this.grafanaConfigDao = grafanaConfigDao; + this.restTemplate = restTemplate; + } + + @PostConstruct + public void init() { + this.url = grafanaProperties.getUrl(); + this.username = grafanaProperties.username(); + this.password = grafanaProperties.password(); + this.prefix = grafanaProperties.getPrefix(); + } + + /** + * Creates a new service admin account id. + * + * @return ResponseEntity containing the result of the account creation + */ + public Long createServiceAccount() { + JsonNode accounts = Objects.requireNonNull(JsonUtil.fromJson(getAccounts().getBody())).path("serviceAccounts"); + for (JsonNode account : accounts) { + if (account.get("name").asText().equals(ACCOUNT_NAME)) { + return account.get("id").asLong(); + } + } + String endpoint = String.format(prefix + CREATE_SERVICE_ACCOUNT_API, url); + HttpHeaders headers = createHeaders(); + String body = String.format("{\"name\":\"%s\",\"role\":\"%s\",\"isDisabled\":false}", ACCOUNT_NAME, ACCOUNT_ROLE); + restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(username, password)); + HttpEntity<String> request = new HttpEntity<>(body, headers); + try { + ResponseEntity<String> response = restTemplate.postForEntity(endpoint, request, String.class); + if (response.getStatusCode().is2xxSuccessful()) { + JsonNode jsonNode = JsonUtil.fromJson(response.getBody()); + if (jsonNode != null && jsonNode.has("id")) { + return jsonNode.get("id").asLong(); + } + } + } catch (Exception e) { + log.error("Service account creation failed", e); + throw new RuntimeException("Service account creation failed"); + } + return null; + } + + /** + * Creates a new API token for a service account. + * + */ + public String applyForToken() { + Long accountId = createServiceAccount(); + if (accountId == null) { + log.error("Service account not found"); + throw new RuntimeException("Service account not found"); + } + String endpoint = String.format(prefix + CREATE_SERVICE_TOKEN_API, url, accountId); + HttpHeaders headers = createHeaders(); + String body = String.format("{\"name\":\"%s\"}", CommonUtil.generateRandomWord(6)); + restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(username, password)); + HttpEntity<String> request = new HttpEntity<>(body, headers); + try { + ResponseEntity<String> response = restTemplate.postForEntity(endpoint, request, String.class); + if (response.getStatusCode().is2xxSuccessful()) { + JsonNode jsonNode = JsonUtil.fromJson(response.getBody()); + if (jsonNode != null && jsonNode.has("key")) { + String token = jsonNode.get("key").asText(); + GrafanaConfig grafanaConfig = GrafanaConfig.builder().token(token).build(); + GeneralConfig generalConfig = GeneralConfig.builder().type(GRAFANA_CONFIG) + .content(JsonUtil.toJson(grafanaConfig)).build(); + grafanaConfigDao.save(generalConfig); + return token; + } + log.info("Create token success: {}", response.getBody()); + } + } catch (Exception e) { + log.error("Create token error", e); + throw new RuntimeException("Create token error"); + } + return null; + } + + /** + * Retrieves the token for a service account. + * + * @return The token key + */ + public String getToken() { + GeneralConfig generalConfig = grafanaConfigDao.findByType(GRAFANA_CONFIG); + if (generalConfig == null) { + log.error("Service token not found"); + return null; + } + GrafanaConfig grafanaConfig = JsonUtil.fromJson(generalConfig.getContent(), GrafanaConfig.class); + if (grafanaConfig == null) { + log.error("Service token not found"); + return null; + } + return grafanaConfig.getToken(); + } + + /** + * Retrieves all service accounts. + * + * @return ResponseEntity containing the list of service accounts + */ + public ResponseEntity<String> getAccounts() { + String endpoint = String.format(prefix + GET_SERVICE_ACCOUNTS_API, url); + HttpHeaders headers = createHeaders(); + restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(username, password)); + HttpEntity<String> request = new HttpEntity<>(headers); + try { + ResponseEntity<String> response = restTemplate.exchange(endpoint, HttpMethod.GET, request, String.class); + if (response.getStatusCode().is2xxSuccessful()) { + log.info("Get accounts success"); + } + return response; + } catch (Exception e) { + log.error("Get accounts error", e); + throw new RuntimeException("Get accounts error"); + } + } + + private HttpHeaders createHeaders() { + String auth = username + SignConstants.DOUBLE_MARK + password; + String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); + String authHeader = NetworkConstants.BASIC + + SignConstants.BLANK + encodedAuth; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set(NetworkConstants.AUTHORIZATION, authHeader); + return headers; + } +} diff --git a/hertzbeat-grafana/src/main/resources/META-INF/spring.factories b/hertzbeat-grafana/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..d621ea1 --- /dev/null +++ b/hertzbeat-grafana/src/main/resources/META-INF/spring.factories @@ -0,0 +1,17 @@ +# 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. + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.hertzbeat.grafana.config.GrafanaAutoConfiguration \ No newline at end of file diff --git a/hertzbeat-grafana/src/test/java/org/apache/hertzbeat/grafana/service/DashboardServiceTest.java b/hertzbeat-grafana/src/test/java/org/apache/hertzbeat/grafana/service/DashboardServiceTest.java new file mode 100644 index 0000000..292f1d7 --- /dev/null +++ b/hertzbeat-grafana/src/test/java/org/apache/hertzbeat/grafana/service/DashboardServiceTest.java @@ -0,0 +1,176 @@ +/* + * 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.grafana.service; + +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard; +import org.apache.hertzbeat.grafana.config.GrafanaProperties; +import org.apache.hertzbeat.grafana.dao.DashboardDao; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +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 static org.mockito.Mockito.lenient; + +/** + * Test case for {@link DashboardService} + */ +@ExtendWith(MockitoExtension.class) +public class DashboardServiceTest { + + @Mock + private ServiceAccountService serviceAccountService; + + @Mock + private DashboardDao dashboardDao; + + @Mock + private GrafanaProperties grafanaProperties; + + @Mock + private RestTemplate restTemplate; + + @Mock + private DatasourceService datasourceService; + + @Mock + private GeneralConfigDao generalConfigDao; + + @InjectMocks + private DashboardService dashboardService; + + static final String GRAFANA_API_RESULT = "{" + + " \"folderUid\": \"\"," + + " \"id\": 3," + + " \"slug\": \"prometheus-dashboard\"," + + " \"status\": \"success\"," + + " \"uid\": \"5d7d89b0-b273-40fe-bb30-d652b82f47eb\"," + + " \"url\": \"/d/5d7d89b0-b273-40fe-bb30-d652b82f47eb/prometheus-dashboard\"," + + " \"version\": 3" + + "}"; + + + + @BeforeEach + void setUp() { + lenient().when(datasourceService.getCurrentDatasourceName()).thenReturn("hertzbeat-vm-localhost-8428"); + lenient().when(grafanaProperties.enabled()).thenReturn(Boolean.TRUE); + lenient().when(grafanaProperties.getPrefix()).thenReturn(""); + lenient().when(grafanaProperties.getUrl()).thenReturn("http://127.0.0.1:3000"); + lenient().when(grafanaProperties.exposeUrl()).thenReturn("http://127.0.0.1:3000"); + lenient().when(serviceAccountService.getToken()).thenReturn("test-token"); + } + + + @Test + void testCreateOrUpdateDashboard() { + ResponseEntity<String> responseEntity = new ResponseEntity<>(GRAFANA_API_RESULT, HttpStatus.OK); + + when(restTemplate.postForEntity( + eq("http://127.0.0.1:3000/api/dashboards/db"), any(HttpEntity.class), eq(String.class) + )).thenReturn(responseEntity); + + ArgumentCaptor<GrafanaDashboard> dashboardCaptor = ArgumentCaptor.forClass(GrafanaDashboard.class); + dashboardService.createOrUpdateDashboard("{\"id\":11}", 1L); + verify(dashboardDao).save(dashboardCaptor.capture()); + + GrafanaDashboard savedDashboard = dashboardCaptor.getValue(); + assertNotNull(savedDashboard); + assertNotNull(savedDashboard.getUrl()); + + String expectedBaseUrl = "http://127.0.0.1:3000/d/5d7d89b0-b273-40fe-bb30-d652b82f47eb"; + + // Verify that the URL begins with the expected base URL. + assertTrue(savedDashboard.getUrl().startsWith(expectedBaseUrl), "URL should start with: " + expectedBaseUrl + ", but was: " + savedDashboard.getUrl()); + + assertTrue(savedDashboard.getUrl().contains("kiosk=tv"), "URL should contain kiosk parameter"); + assertTrue(savedDashboard.getUrl().contains("refresh=15s"), "URL should contain refresh parameter"); + assertTrue(savedDashboard.getUrl().contains("var-instance=1"), "URL should contain instance parameter"); + } + + @Test + void testCreateOrUpdateDashboardWithTrailingSlash() { + when(grafanaProperties.exposeUrl()).thenReturn("http://127.0.0.1:3000/"); + + ResponseEntity<String> responseEntity = new ResponseEntity<>(GRAFANA_API_RESULT, HttpStatus.OK); + + when(restTemplate.postForEntity( + eq("http://127.0.0.1:3000/api/dashboards/db"), any(HttpEntity.class), eq(String.class) + )).thenReturn(responseEntity); + + ArgumentCaptor<GrafanaDashboard> dashboardCaptor = ArgumentCaptor.forClass(GrafanaDashboard.class); + dashboardService.createOrUpdateDashboard("{\"id\":11}", 1L); + verify(dashboardDao).save(dashboardCaptor.capture()); + + GrafanaDashboard savedDashboard = dashboardCaptor.getValue(); + assertNotNull(savedDashboard); + assertNotNull(savedDashboard.getUrl()); + + String expectedBaseUrl = "http://127.0.0.1:3000/d/5d7d89b0-b273-40fe-bb30-d652b82f47eb"; + + // Verify that the URL begins with the expected base URL. + assertTrue(savedDashboard.getUrl().startsWith(expectedBaseUrl), "URL should start with: " + expectedBaseUrl + ", but was: " + savedDashboard.getUrl()); + + assertTrue(savedDashboard.getUrl().contains("kiosk=tv"), "URL should contain kiosk parameter"); + assertTrue(savedDashboard.getUrl().contains("refresh=15s"), "URL should contain refresh parameter"); + assertTrue(savedDashboard.getUrl().contains("var-instance=1"), "URL should contain instance parameter"); + } + + + @Test + void testGetDashboardByMonitorId() { + Long monitorId = 1L; + String originalUrl = "http://localhost:3000/d/123/my-dashboard?kiosk"; + GrafanaDashboard originalDashboard = GrafanaDashboard.builder() + .monitorId(monitorId) + .url(originalUrl) + .build(); + + when(dashboardDao.findByMonitorId(eq(monitorId))).thenReturn(originalDashboard); + + String themeConfigJson = "{\"theme\":\"light\"}"; + org.apache.hertzbeat.common.entity.manager.GeneralConfig runConfig = new org.apache.hertzbeat.common.entity.manager.GeneralConfig(); + runConfig.setContent(themeConfigJson); + + when(generalConfigDao.findByType(eq(org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum.system.name()))) + .thenReturn(runConfig); + + GrafanaDashboard resultDashboard = dashboardService.getDashboardByMonitorId(monitorId); + + assertNotNull(resultDashboard); + assertTrue(resultDashboard.getUrl().contains("&theme=light")); + assertTrue(originalDashboard.getUrl().endsWith("?kiosk")); + assertFalse(originalDashboard.getUrl().contains("&theme=")); + } + +} diff --git a/hertzbeat-log/pom.xml b/hertzbeat-log/pom.xml new file mode 100644 index 0000000..7bc9c65 --- /dev/null +++ b/hertzbeat-log/pom.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat</artifactId> + <version>2.0-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>hertzbeat-log</artifactId> + <name>${project.artifactId}</name> + + <properties> + <awaitility.version>4.2.0</awaitility.version> + </properties> + + <dependencies> + <!-- common --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-base</artifactId> + <scope>provided</scope> + </dependency> + <!-- warehouse --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-warehouse</artifactId> + </dependency> + <!-- spring --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + <scope>provided</scope> + </dependency> + <!-- swagger --> + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <scope>provided</scope> + </dependency> + <!-- opentelemetry proto --> + <dependency> + <groupId>io.opentelemetry.proto</groupId> + <artifactId>opentelemetry-proto</artifactId> + </dependency> + <!-- protobuf dependencies for OTLP processing --> + <dependency> + <groupId>com.google.protobuf</groupId> + <artifactId>protobuf-java</artifactId> + </dependency> + <!-- protobuf java util --> + <dependency> + <groupId>com.google.protobuf</groupId> + <artifactId>protobuf-java-util</artifactId> + </dependency> + <!-- awaitility for async testing --> + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility</artifactId> + <version>${awaitility.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + +</project> \ No newline at end of file diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogIngestionController.java b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogIngestionController.java new file mode 100644 index 0000000..e32510e --- /dev/null +++ b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogIngestionController.java @@ -0,0 +1,82 @@ +/* + * 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 lombok.extern.slf4j.Slf4j; +import java.util.List; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.log.service.LogProtocolAdapter; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Generic Log Ingestion Controller + * Provides a fallback endpoint for log protocols that don't have dedicated controllers. + * For OTLP protocol, use OtlpLogController instead. + */ +@Tag(name = "Log Ingestion Controller") +@RestController +@RequestMapping(path = "/api/logs", produces = MediaType.APPLICATION_JSON_VALUE) +@Slf4j +public class LogIngestionController { + + private final List<LogProtocolAdapter> protocolAdapters; + + public LogIngestionController(List<LogProtocolAdapter> protocolAdapters) { + this.protocolAdapters = protocolAdapters; + } + + /** + * Receive log payload pushed from external system specifying the log protocol. + * + * @param protocol log protocol identifier (e.g., "vector", "loki") + * @param content raw request body + */ + @Operation(summary = "Ingest logs by protocol name") + @PostMapping(value = "/ingest/{protocol}", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity<Message<Void>> ingestLog(@PathVariable("protocol") String protocol, + @RequestBody String content) { + log.debug("Receive log from protocol: {}, content length: {}", protocol, content == null ? 0 : content.length()); + + for (LogProtocolAdapter adapter : protocolAdapters) { + if (adapter.supportProtocol().equalsIgnoreCase(protocol)) { + try { + adapter.ingest(content); + return ResponseEntity.ok(Message.success("Add extern log success")); + } catch (Exception e) { + log.error("Add log failed: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Message.fail(CommonConstants.FAIL_CODE, "Add extern log failed: " + e.getMessage())); + } + } + } + log.warn("Not support extern log from protocol: {}", protocol); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Message.fail(CommonConstants.FAIL_CODE, "Not support the " + protocol + " protocol log")); + } +} diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogManagerController.java b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogManagerController.java new file mode 100644 index 0000000..f552e5c --- /dev/null +++ b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogManagerController.java @@ -0,0 +1,64 @@ +/* + * 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.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataWriter; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE; + +/** + * Controller for managing log entries in HertzBeat. + */ +@RestController +@RequestMapping(path = "/api/logs", produces = "application/json") +@Tag(name = "Log Management Controller") +@Slf4j +public class LogManagerController { + + private final HistoryDataWriter historyDataWriter; + + public LogManagerController(HistoryDataWriter historyDataWriter) { + this.historyDataWriter = historyDataWriter; + } + + @DeleteMapping + @Operation(summary = "Batch delete logs", + description = "Batch delete logs by time timestamps. Deletes multiple log entries based on their Unix nanosecond timestamps.") + public ResponseEntity<Message<String>> batchDelete( + @Parameter(description = "List of Unix nanosecond timestamps for logs to delete", example = "1640995200000000000") + @RequestParam(required = false) List<Long> timeUnixNanos) { + boolean result = historyDataWriter.batchDeleteLogs(timeUnixNanos); + if (result) { + return ResponseEntity.ok(Message.success("Logs deleted successfully")); + } else { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "Failed to delete logs")); + } + } +} diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogQueryController.java b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogQueryController.java new file mode 100644 index 0000000..5d0bed1 --- /dev/null +++ b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogQueryController.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.log.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.entity.log.LogEntry; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Log query and statistics APIs for UI consumption + */ +@RestController +@RequestMapping(path = "/api/logs", produces = "application/json") +@Tag(name = "Log Query Controller") +@Slf4j +public class LogQueryController { + + private final HistoryDataReader historyDataReader; + + public LogQueryController(HistoryDataReader historyDataReader) { + this.historyDataReader = historyDataReader; + } + + @GetMapping("/list") + @Operation(summary = "Query logs by time range with optional filters", + description = "Query logs by [start,end] in ms and optional filters with pagination. Returns paginated log entries sorted by timestamp in descending order.") + public ResponseEntity<Message<Page<LogEntry>>> list( + @Parameter(description = "Start timestamp in milliseconds (Unix timestamp)", example = "1640995200000") + @RequestParam(value = "start", required = false) Long start, + @Parameter(description = "End timestamp in milliseconds (Unix timestamp)", example = "1641081600000") + @RequestParam(value = "end", required = false) Long end, + @Parameter(description = "Trace ID for distributed tracing", example = "1234567890abcdef") + @RequestParam(value = "traceId", required = false) String traceId, + @Parameter(description = "Span ID for distributed tracing", example = "abcdef1234567890") + @RequestParam(value = "spanId", required = false) String spanId, + @Parameter(description = "Log severity number (1-24 according to OpenTelemetry standard)", example = "9") + @RequestParam(value = "severityNumber", required = false) Integer severityNumber, + @Parameter(description = "Log severity text (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)", example = "INFO") + @RequestParam(value = "severityText", required = false) String severityText, + @Parameter(description = "Log content search keyword", example = "error") + @RequestParam(value = "search", required = false) String search, + @Parameter(description = "Page index starting from 0", example = "0") + @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) { + Page<LogEntry> result = getPagedLogs(start, end, traceId, spanId, severityNumber, severityText, search, pageIndex, pageSize); + return ResponseEntity.ok(Message.success(result)); + } + + @GetMapping("/stats/overview") + @Operation(summary = "Log overview statistics", + description = "Overall counts and basic statistics with filters. Provides counts by severity levels according to OpenTelemetry standard.") + public ResponseEntity<Message<Map<String, Object>>> overviewStats( + @Parameter(description = "Start timestamp in milliseconds (Unix timestamp)", example = "1640995200000") + @RequestParam(value = "start", required = false) Long start, + @Parameter(description = "End timestamp in milliseconds (Unix timestamp)", example = "1641081600000") + @RequestParam(value = "end", required = false) Long end, + @Parameter(description = "Trace ID for distributed tracing", example = "1234567890abcdef") + @RequestParam(value = "traceId", required = false) String traceId, + @Parameter(description = "Span ID for distributed tracing", example = "abcdef1234567890") + @RequestParam(value = "spanId", required = false) String spanId, + @Parameter(description = "Log severity number (1-24 according to OpenTelemetry standard)", example = "9") + @RequestParam(value = "severityNumber", required = false) Integer severityNumber, + @Parameter(description = "Log severity text (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)", example = "INFO") + @RequestParam(value = "severityText", required = false) String severityText, + @Parameter(description = "Log content search keyword", example = "error") + @RequestParam(value = "search", required = false) String search) { + List<LogEntry> logs = getFilteredLogs(start, end, traceId, spanId, severityNumber, severityText, search); + + Map<String, Object> overview = new HashMap<>(); + overview.put("totalCount", logs.size()); + + // Count by severity levels according to OpenTelemetry standard + // TRACE: 1-4, DEBUG: 5-8, INFO: 9-12, WARN: 13-16, ERROR: 17-20, FATAL: 21-24 + long fatalCount = logs.stream().filter(log -> log.getSeverityNumber() != null && log.getSeverityNumber() >= 21 && log.getSeverityNumber() <= 24).count(); + long errorCount = logs.stream().filter(log -> log.getSeverityNumber() != null && log.getSeverityNumber() >= 17 && log.getSeverityNumber() <= 20).count(); + long warnCount = logs.stream().filter(log -> log.getSeverityNumber() != null && log.getSeverityNumber() >= 13 && log.getSeverityNumber() <= 16).count(); + long infoCount = logs.stream().filter(log -> log.getSeverityNumber() != null && log.getSeverityNumber() >= 9 && log.getSeverityNumber() <= 12).count(); + long debugCount = logs.stream().filter(log -> log.getSeverityNumber() != null && log.getSeverityNumber() >= 5 && log.getSeverityNumber() <= 8).count(); + long traceCount = logs.stream().filter(log -> log.getSeverityNumber() != null && log.getSeverityNumber() >= 1 && log.getSeverityNumber() <= 4).count(); + + overview.put("fatalCount", fatalCount); + overview.put("errorCount", errorCount); + overview.put("warnCount", warnCount); + overview.put("infoCount", infoCount); + overview.put("debugCount", debugCount); + overview.put("traceCount", traceCount); + + return ResponseEntity.ok(Message.success(overview)); + } + + @GetMapping("/stats/trace-coverage") + @Operation(summary = "Trace coverage statistics", + description = "Statistics about trace information availability. Shows how many logs have trace IDs, span IDs, or both for distributed tracing analysis.") + public ResponseEntity<Message<Map<String, Object>>> traceCoverageStats( + @Parameter(description = "Start timestamp in milliseconds (Unix timestamp)", example = "1640995200000") + @RequestParam(value = "start", required = false) Long start, + @Parameter(description = "End timestamp in milliseconds (Unix timestamp)", example = "1641081600000") + @RequestParam(value = "end", required = false) Long end, + @Parameter(description = "Trace ID for distributed tracing", example = "1234567890abcdef") + @RequestParam(value = "traceId", required = false) String traceId, + @Parameter(description = "Span ID for distributed tracing", example = "abcdef1234567890") + @RequestParam(value = "spanId", required = false) String spanId, + @Parameter(description = "Log severity number (1-24 according to OpenTelemetry standard)", example = "9") + @RequestParam(value = "severityNumber", required = false) Integer severityNumber, + @Parameter(description = "Log severity text (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)", example = "INFO") + @RequestParam(value = "severityText", required = false) String severityText, + @Parameter(description = "Log content search keyword", example = "error") + @RequestParam(value = "search", required = false) String search) { + List<LogEntry> logs = getFilteredLogs(start, end, traceId, spanId, severityNumber, severityText, search); + + Map<String, Object> result = new HashMap<>(); + + // Trace coverage statistics + long withTraceId = logs.stream().filter(log -> log.getTraceId() != null && !log.getTraceId().isEmpty()).count(); + long withSpanId = logs.stream().filter(log -> log.getSpanId() != null && !log.getSpanId().isEmpty()).count(); + long withBothTraceAndSpan = logs.stream().filter(log -> + log.getTraceId() != null && !log.getTraceId().isEmpty() + && log.getSpanId() != null && !log.getSpanId().isEmpty()).count(); + long withoutTrace = logs.size() - withTraceId; + + Map<String, Long> traceCoverage = new HashMap<>(); + traceCoverage.put("withTrace", withTraceId); + traceCoverage.put("withoutTrace", withoutTrace); + traceCoverage.put("withSpan", withSpanId); + traceCoverage.put("withBothTraceAndSpan", withBothTraceAndSpan); + + result.put("traceCoverage", traceCoverage); + return ResponseEntity.ok(Message.success(result)); + } + + @GetMapping("/stats/trend") + @Operation(summary = "Log trend over time", + description = "Count logs by hour intervals with filters. Groups logs by hour and provides time-series data for trend analysis.") + public ResponseEntity<Message<Map<String, Object>>> trendStats( + @Parameter(description = "Start timestamp in milliseconds (Unix timestamp)", example = "1640995200000") + @RequestParam(value = "start", required = false) Long start, + @Parameter(description = "End timestamp in milliseconds (Unix timestamp)", example = "1641081600000") + @RequestParam(value = "end", required = false) Long end, + @Parameter(description = "Trace ID for distributed tracing", example = "1234567890abcdef") + @RequestParam(value = "traceId", required = false) String traceId, + @Parameter(description = "Span ID for distributed tracing", example = "abcdef1234567890") + @RequestParam(value = "spanId", required = false) String spanId, + @Parameter(description = "Log severity number (1-24 according to OpenTelemetry standard)", example = "9") + @RequestParam(value = "severityNumber", required = false) Integer severityNumber, + @Parameter(description = "Log severity text (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)", example = "INFO") + @RequestParam(value = "severityText", required = false) String severityText, + @Parameter(description = "Log content search keyword", example = "error") + @RequestParam(value = "search", required = false) String search) { + List<LogEntry> logs = getFilteredLogs(start, end, traceId, spanId, severityNumber, severityText, search); + + // Group by hour + Map<String, Long> hourlyStats = logs.stream() + .filter(log -> log.getTimeUnixNano() != null) + .collect(Collectors.groupingBy( + log -> { + long timestampMs = log.getTimeUnixNano() / 1_000_000L; + LocalDateTime dateTime = LocalDateTime.ofInstant( + Instant.ofEpochMilli(timestampMs), + ZoneId.systemDefault()); + return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00")); + }, + Collectors.counting())); + + Map<String, Object> result = new HashMap<>(); + result.put("hourlyStats", hourlyStats); + return ResponseEntity.ok(Message.success(result)); + } + + private List<LogEntry> getFilteredLogs(Long start, Long end, String traceId, String spanId, + Integer severityNumber, String severityText, String search) { + // Use the new multi-condition query method + return historyDataReader.queryLogsByMultipleConditions(start, end, traceId, spanId, severityNumber, severityText, search); + } + + private Page<LogEntry> getPagedLogs(Long start, Long end, String traceId, String spanId, + Integer severityNumber, String severityText, String search, + Integer pageIndex, Integer pageSize) { + // Calculate pagination parameters + int offset = pageIndex * pageSize; + + // Get total count and paginated data + long totalElements = historyDataReader.countLogsByMultipleConditions(start, end, traceId, spanId, severityNumber, severityText, search); + List<LogEntry> pagedLogs = historyDataReader.queryLogsByMultipleConditionsWithPagination( + start, end, traceId, spanId, severityNumber, severityText, search, offset, pageSize); + + // Create PageRequest (sorted by timestamp descending) + Sort sort = Sort.by(Sort.Direction.DESC, "timeUnixNano"); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sort); + + // Return Spring Data Page object + return new PageImpl<>(pagedLogs, pageRequest, totalElements); + } +} \ No newline at end of file diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogSseController.java b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogSseController.java new file mode 100644 index 0000000..e519ae0 --- /dev/null +++ b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogSseController.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.log.controller; + +import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE; +import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator; +import org.apache.hertzbeat.log.notice.LogSseFilterCriteria; +import org.apache.hertzbeat.log.notice.LogSseManager; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import io.swagger.v3.oas.annotations.Operation; + +/** + * SSE controller for log streaming with filtering support + */ +@RestController +@RequestMapping(path = "/api/logs/sse", produces = {TEXT_EVENT_STREAM_VALUE}) +public class LogSseController { + + private final LogSseManager emitterManager; + + public LogSseController(LogSseManager emitterManager) { + this.emitterManager = emitterManager; + } + + /** + * Subscribe to log events with optional filtering + * @param filterCriteria Filter criteria for log events (all parameters are optional) + * @return SSE emitter for streaming log events + */ + @GetMapping(path = "/subscribe") + @Operation(summary = "Subscribe to log events with optional filtering", description = "Subscribe to log events with optional filtering") + public SseEmitter subscribe(@ModelAttribute LogSseFilterCriteria filterCriteria) { + Long clientId = SnowFlakeIdGenerator.generateId(); + return emitterManager.createEmitter(clientId, filterCriteria); + } +} \ No newline at end of file diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/OtlpLogController.java b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/OtlpLogController.java new file mode 100644 index 0000000..3d01075 --- /dev/null +++ b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/OtlpLogController.java @@ -0,0 +1,160 @@ +/* + * 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 com.fasterxml.jackson.core.io.JsonStringEncoder; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import com.google.rpc.Status; +import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.log.service.impl.OtlpLogProtocolAdapter; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * OTLP Log Ingestion Controller + * Implements OTLP/HTTP specification for log ingestion. + * Supports both binary-encoded Protobuf (application/x-protobuf) and JSON-encoded Protobuf (application/json). + * + * @see <a href="https://opentelemetry.io/docs/specs/otlp/#otlphttp">OTLP/HTTP Specification</a> + */ +@Tag(name = "OTLP Log Controller") +@RestController +@RequestMapping(path = "/api/logs/otlp") +@Slf4j +public class OtlpLogController { + + private static final String CONTENT_TYPE_PROTOBUF = "application/x-protobuf"; + + private static final ExportLogsServiceResponse EMPTY_RESPONSE = ExportLogsServiceResponse.newBuilder().build(); + + private final OtlpLogProtocolAdapter otlpLogProtocolAdapter; + + public OtlpLogController(OtlpLogProtocolAdapter otlpLogProtocolAdapter) { + this.otlpLogProtocolAdapter = otlpLogProtocolAdapter; + } + + /** + * OTLP/HTTP standard endpoint for logs with JSON-encoded Protobuf payload. + * Content-Type: application/json + * + * Response follows OTLP specification: + * - Success: HTTP 200 with ExportLogsServiceResponse (JSON encoded) + * - Failure: HTTP 400 with google.rpc.Status (JSON encoded) + * + * @param content JSON-encoded ExportLogsServiceRequest + * @return ExportLogsServiceResponse on success, Status on failure + */ + @Operation(summary = "Ingest OTLP logs (JSON format)") + @PostMapping(value = "/v1/logs", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity<String> ingestJsonLogs(@RequestBody String content) { + log.debug("Receive OTLP JSON logs, content length: {}", content == null ? 0 : content.length()); + try { + otlpLogProtocolAdapter.ingest(content); + return ResponseEntity.ok(toJsonResponse(EMPTY_RESPONSE)); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(toJsonErrorResponse(e.getMessage())); + } catch (Exception e) { + // Server-side errors - unexpected failure + log.error("Unexpected error ingesting OTLP JSON logs: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(toJsonErrorResponse(e.getMessage())); + } + } + + /** + * OTLP/HTTP standard endpoint for logs with binary-encoded Protobuf payload. + * Content-Type: application/x-protobuf + * + * Response follows OTLP specification: + * - Success: HTTP 200 with ExportLogsServiceResponse (binary encoded) + * - Failure: HTTP 400 with google.rpc.Status (binary encoded) + * + * @param content binary-encoded ExportLogsServiceRequest + * @return ExportLogsServiceResponse on success, Status on failure + */ + @Operation(summary = "Ingest OTLP logs (binary Protobuf format)") + @PostMapping(value = "/v1/logs", consumes = CONTENT_TYPE_PROTOBUF, produces = CONTENT_TYPE_PROTOBUF) + public ResponseEntity<byte[]> ingestBinaryLogs(@RequestBody byte[] content) { + log.debug("Receive OTLP binary logs, content length: {}", content == null ? 0 : content.length); + try { + otlpLogProtocolAdapter.ingestBinary(content); + return ResponseEntity.ok(EMPTY_RESPONSE.toByteArray()); + } catch (IllegalArgumentException e) { + // Client-side validation errors - malformed request + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(createBinaryErrorResponse(e.getMessage())); + } catch (Exception e) { + // Server-side errors - unexpected failure + log.error("Unexpected error ingesting OTLP binary logs: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(createBinaryErrorResponse(e.getMessage())); + } + } + + private String toJsonResponse(ExportLogsServiceResponse response) { + try { + return JsonFormat.printer().print(response); + } catch (InvalidProtocolBufferException e) { + log.error("Failed to convert ExportLogsServiceResponse to JSON: {}", e.getMessage(), e); + return "{}"; + } + } + + private String toJsonErrorResponse(String message) { + Status status = Status.newBuilder() + .setMessage(message != null ? message : "Unknown error") + .build(); + try { + return JsonFormat.printer().print(status); + } catch (InvalidProtocolBufferException e) { + return "{\"message\":\"" + escapeJson(message) + "\"}"; + } + } + + /** + * Escapes a string value for safe inclusion in JSON. + * + * @param message the string to escape + * @return the escaped string, or empty string if message is null + */ + private String escapeJson(String message) { + if (message == null) { + return ""; + } + + char[] escaped = JsonStringEncoder.getInstance().quoteAsString(message); + return new String(escaped); + } + + private byte[] createBinaryErrorResponse(String message) { + return Status.newBuilder() + .setMessage(message != null ? message : "Unknown error") + .build() + .toByteArray(); + } +} diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/notice/LogSseFilterCriteria.java b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/notice/LogSseFilterCriteria.java new file mode 100644 index 0000000..b42f68a --- /dev/null +++ b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/notice/LogSseFilterCriteria.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.log.notice; + +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.springframework.util.StringUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; + +/** + * Log filtering criteria for SSE (Server-Sent Events) log streaming + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Log filtering criteria for SSE (Server-Sent Events) log streaming") +public class LogSseFilterCriteria { + /** + * Numerical value of the severity. + * Smaller numerical values correspond to less severe events (such as debug events), + * larger numerical values correspond to more severe events (such as errors and critical events). + */ + @Schema(description = "Numerical value of the severity.", example = "1", accessMode = READ_WRITE) + private Integer severityNumber; + + /** + * The severity text (also known as log level). + * This is the original string representation of the severity as it is known at the source. + */ + @Schema(description = "The severity text (also known as log level).", example = "INFO", accessMode = READ_WRITE) + private String severityText; + + /** + * Log content text filtering (case-insensitive contains match). + */ + @Schema(description = "Log content text filtering", example = "error occurred", accessMode = READ_WRITE) + private String logContent; + + /** + * A unique identifier for a trace. + * All spans from the same trace share the same trace_id. + * The ID is a 16-byte array represented as a hex string. + */ + @Schema(description = "A unique identifier for a trace.", example = "1234567890", accessMode = READ_WRITE) + private String traceId; + + /** + * A unique identifier for a span within a trace. + * The ID is an 8-byte array represented as a hex string. + */ + @Schema(description = "A unique identifier for a span.", example = "1234567890", accessMode = READ_WRITE) + private String spanId; + + + /** + * Core filtering logic to determine if a log entry matches the criteria + * @param log Log entry to be checked + * @return boolean Whether the log entry matches the filter criteria + */ + public boolean matches(LogEntry log) { + if (log == null) return false; + // Check severity text match + if (StringUtils.hasText(severityText) && !severityText.equalsIgnoreCase(log.getSeverityText())) { + return false; + } + + // Check severity number match (if both are present) + if (severityNumber != null && log.getSeverityNumber() != null + && !severityNumber.equals(log.getSeverityNumber())) { + return false; + } + + // Check log content match + if (StringUtils.hasText(logContent)) { + Object body = log.getBody(); + if (body == null) { + return false; + } + String bodyStr = body.toString(); + if (!StringUtils.hasText(bodyStr) || !bodyStr.toLowerCase().contains(logContent.toLowerCase())) { + return false; + } + } + + // Check trace ID match + if (StringUtils.hasText(traceId) && !traceId.equalsIgnoreCase(log.getTraceId())) { + return false; + } + + // Check span ID match + if (StringUtils.hasText(spanId) && !spanId.equalsIgnoreCase(log.getSpanId())) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/notice/LogSseManager.java b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/notice/LogSseManager.java new file mode 100644 index 0000000..d45015a --- /dev/null +++ b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/notice/LogSseManager.java @@ -0,0 +1,209 @@ +/* + * 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.notice; + +import jakarta.annotation.PreDestroy; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * SSE manager for log with batch processing support for high TPS scenarios + */ +@Component +@Slf4j +@Getter +public class LogSseManager { + + private static final long BATCH_INTERVAL_MS = 200; + private static final int MAX_BATCH_SIZE = 1000; + private static final int MAX_QUEUE_SIZE = 10000; + + private final Map<Long, SseSubscriber> emitters = new ConcurrentHashMap<>(); + private final Queue<LogEntry> logQueue = new ConcurrentLinkedQueue<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "sse-batch-scheduler"); + t.setDaemon(true); + return t; + }); + private final ExecutorService senderPool = Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name("sse-sender-", 0) + .uncaughtExceptionHandler((thread, throwable) -> + log.error("SSE sender has uncaughtException.", throwable)) + .factory()); + private final AtomicLong queueSize = new AtomicLong(0); + + public LogSseManager() { + scheduler.scheduleAtFixedRate(this::flushBatch, BATCH_INTERVAL_MS, BATCH_INTERVAL_MS, TimeUnit.MILLISECONDS); + } + + @PreDestroy + public void shutdown() { + scheduler.shutdown(); + senderPool.shutdown(); + try { + scheduler.awaitTermination(2, TimeUnit.SECONDS); + senderPool.awaitTermination(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + scheduler.shutdownNow(); + senderPool.shutdownNow(); + } + + /** + * Create a new SSE emitter for a client with specified filters + */ + public SseEmitter createEmitter(Long clientId, LogSseFilterCriteria filters) { + SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); + emitter.onCompletion(() -> removeEmitter(clientId)); + emitter.onTimeout(() -> removeEmitter(clientId)); + emitter.onError((ex) -> removeEmitter(clientId)); + + emitters.put(clientId, new SseSubscriber(emitter, filters)); + return emitter; + } + + /** + * Queue log entry for batch processing + */ + public void broadcast(LogEntry logEntry) { + if (queueSize.incrementAndGet() > MAX_QUEUE_SIZE) { + queueSize.decrementAndGet(); + return; + } + boolean offered = logQueue.offer(logEntry); + if (!offered) { + queueSize.decrementAndGet(); + log.warn("Failed to enqueue log entry: {}", logEntry); + } + } + + /** + * Flush queued logs to all subscribers in batch + */ + private void flushBatch() { + try { + if (logQueue.isEmpty() || emitters.isEmpty()) { + return; + } + + List<LogEntry> batch = new ArrayList<>(MAX_BATCH_SIZE); + LogEntry entry; + while (batch.size() < MAX_BATCH_SIZE && (entry = logQueue.poll()) != null) { + batch.add(entry); + queueSize.decrementAndGet(); + } + + if (batch.isEmpty()) { + return; + } + + // Send to each subscriber in parallel + for (Map.Entry<Long, SseSubscriber> e : emitters.entrySet()) { + Long clientId = e.getKey(); + SseSubscriber subscriber = e.getValue(); + List<LogEntry> filtered = filterLogs(batch, subscriber.filters); + if (!filtered.isEmpty()) { + senderPool.submit(() -> sendToSubscriber(clientId, subscriber.emitter, filtered)); + } + } + } catch (Exception e) { + log.error("Error in flushBatch: {}", e.getMessage(), e); + } + } + + private void sendToSubscriber(Long clientId, SseEmitter emitter, List<LogEntry> logs) { + try { + long batchTimestamp = System.currentTimeMillis(); + int sequenceNumber = 0; + for (LogEntry logEntry : logs) { + String eventId = batchTimestamp + "-" + sequenceNumber++; + emitter.send(SseEmitter.event() + .id(eventId) + .name("LOG_EVENT") + .data(logEntry)); + } + } catch (IOException | IllegalStateException e) { + safeComplete(clientId, emitter); + } catch (Exception e) { + log.error("Failed to send to client {}: {}", clientId, e.getMessage()); + safeComplete(clientId, emitter); + } + } + + private void safeComplete(Long clientId, SseEmitter emitter) { + try { + emitter.complete(); + } catch (Exception ignored) { + } + removeEmitter(clientId); + } + + private List<LogEntry> filterLogs(List<LogEntry> logs, LogSseFilterCriteria filters) { + if (filters == null) { + return logs; + } + List<LogEntry> filtered = new ArrayList<>(); + for (LogEntry log : logs) { + if (filters.matches(log)) { + filtered.add(log); + } + } + return filtered; + } + + private void removeEmitter(Long clientId) { + emitters.remove(clientId); + } + + public long getQueueSize() { + return queueSize.get(); + } + + /** + * SseSubscriber for SseEmitter and LogSseFilterCriteria + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class SseSubscriber { + private SseEmitter emitter; + private LogSseFilterCriteria filters; + } +} diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/LogProtocolAdapter.java b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/LogProtocolAdapter.java new file mode 100644 index 0000000..a33e861 --- /dev/null +++ b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/LogProtocolAdapter.java @@ -0,0 +1,41 @@ +/* + * 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.service; + +/** + * Adapter interface for ingesting logs pushed via different protocols + * (e.g. OTLP, Loki, Filebeat, Vector). + * Implementations should: + * 1. Parse raw HTTP payload of their protocol. + * 2. Convert data to LogEntry. + * 3. Forward / persist it to downstream pipeline. + */ +public interface LogProtocolAdapter { + + /** + * Ingest log payload pushed from external system. + * + * @param content raw request body string + */ + void ingest(String content); + + /** + * Identifier of the protocol this adapter supports ("otlp", "vector", etc.) + */ + String supportProtocol(); +} \ No newline at end of file diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/impl/OtlpLogProtocolAdapter.java b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/impl/OtlpLogProtocolAdapter.java new file mode 100644 index 0000000..46978af --- /dev/null +++ b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/impl/OtlpLogProtocolAdapter.java @@ -0,0 +1,281 @@ +/* + * 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.service.impl; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.common.v1.KeyValueList; +import io.opentelemetry.proto.logs.v1.LogRecord; +import io.opentelemetry.proto.logs.v1.ResourceLogs; +import io.opentelemetry.proto.logs.v1.ScopeLogs; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.log.notice.LogSseManager; +import org.apache.hertzbeat.log.service.LogProtocolAdapter; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Adapter for OpenTelemetry OTLP/HTTP log ingestion. + * Supports both JSON-encoded and binary-encoded Protobuf formats. + * + * @see <a href="https://opentelemetry.io/docs/specs/otlp/#otlphttp">OTLP/HTTP Specification</a> + */ +@Slf4j +@Service +public class OtlpLogProtocolAdapter implements LogProtocolAdapter { + + private static final String PROTOCOL_NAME = "otlp"; + + private final CommonDataQueue commonDataQueue; + private final LogSseManager logSseManager; + + public OtlpLogProtocolAdapter(CommonDataQueue commonDataQueue, LogSseManager logSseManager) { + this.commonDataQueue = commonDataQueue; + this.logSseManager = logSseManager; + } + + @Override + public void ingest(String content) { + if (content == null || content.isEmpty()) { + log.warn("Received empty OTLP JSON log payload - skip processing."); + return; + } + ExportLogsServiceRequest.Builder builder = ExportLogsServiceRequest.newBuilder(); + try { + JsonFormat.parser().ignoringUnknownFields().merge(content, builder); + ExportLogsServiceRequest request = builder.build(); + processLogsRequest(request, "JSON"); + } catch (InvalidProtocolBufferException e) { + log.error("Failed to parse OTLP JSON log payload: {}", e.getMessage()); + throw new IllegalArgumentException("Invalid OTLP JSON log content", e); + } + } + + /** + * Ingest binary-encoded Protobuf log payload (OTLP-specific). + * + * @param content binary-encoded ExportLogsServiceRequest + */ + public void ingestBinary(byte[] content) { + if (content == null || content.length == 0) { + log.warn("Received empty OTLP binary log payload - skip processing."); + return; + } + try { + ExportLogsServiceRequest request = ExportLogsServiceRequest.parseFrom(content); + processLogsRequest(request, "binary"); + } catch (InvalidProtocolBufferException e) { + log.error("Failed to parse OTLP binary log payload: {}", e.getMessage()); + throw new IllegalArgumentException("Invalid OTLP binary log content", e); + } + } + + private void processLogsRequest(ExportLogsServiceRequest request, String format) { + List<LogEntry> logEntries = extractLogEntries(request); + log.debug("Successfully extracted {} log entries from OTLP {} payload", logEntries.size(), format); + commonDataQueue.sendLogEntryToStorageBatch(logEntries); + commonDataQueue.sendLogEntryToAlertBatch(logEntries); + logEntries.forEach(logSseManager::broadcast); + } + + /** + * Extract LogEntry instances from ExportLogsServiceRequest. + * + * @param request the OTLP export logs service request + * @return list of extracted log entries + */ + private List<LogEntry> extractLogEntries(ExportLogsServiceRequest request) { + List<LogEntry> logEntries = new ArrayList<>(); + + for (ResourceLogs resourceLogs : request.getResourceLogsList()) { + // Extract resource attributes + Map<String, Object> resourceAttributes = extractAttributes( + resourceLogs.getResource().getAttributesList() + ); + + for (ScopeLogs scopeLogs : resourceLogs.getScopeLogsList()) { + // Extract instrumentation scope information + LogEntry.InstrumentationScope instrumentationScope = extractInstrumentationScope(scopeLogs); + + for (LogRecord logRecord : scopeLogs.getLogRecordsList()) { + LogEntry logEntry = convertLogRecordToLogEntry( + logRecord, + resourceAttributes, + instrumentationScope + ); + logEntries.add(logEntry); + } + } + } + + return logEntries; + } + + /** + * Convert OpenTelemetry LogRecord to LogEntry. + */ + private LogEntry convertLogRecordToLogEntry( + LogRecord logRecord, + Map<String, Object> resourceAttributes, + LogEntry.InstrumentationScope instrumentationScope) { + + return LogEntry.builder() + .timeUnixNano(logRecord.getTimeUnixNano()) + .observedTimeUnixNano(logRecord.getObservedTimeUnixNano()) + .severityNumber(logRecord.getSeverityNumberValue()) + .severityText(logRecord.getSeverityText()) + .body(extractBody(logRecord.getBody())) + .attributes(extractAttributes(logRecord.getAttributesList())) + .droppedAttributesCount(logRecord.getDroppedAttributesCount()) + .traceId(bytesToHex(logRecord.getTraceId().toByteArray())) + .spanId(bytesToHex(logRecord.getSpanId().toByteArray())) + .traceFlags(logRecord.getFlags()) + .resource(resourceAttributes) + .instrumentationScope(instrumentationScope) + .build(); + } + + /** + * Extract instrumentation scope information from ScopeLogs. + */ + private LogEntry.InstrumentationScope extractInstrumentationScope(ScopeLogs scopeLogs) { + if (!scopeLogs.hasScope()) { + return null; + } + + var scope = scopeLogs.getScope(); + return LogEntry.InstrumentationScope.builder() + .name(scope.getName()) + .version(scope.getVersion()) + .attributes(extractAttributes(scope.getAttributesList())) + .droppedAttributesCount(scope.getDroppedAttributesCount()) + .build(); + } + + /** + * Extract attributes from a list of KeyValue pairs. + */ + private Map<String, Object> extractAttributes(List<KeyValue> keyValueList) { + if (keyValueList == null || keyValueList.isEmpty()) { + return new HashMap<>(); + } + + AnyValue anyValue = AnyValue.newBuilder() + .setKvlistValue(KeyValueList.newBuilder() + .addAllValues(keyValueList) + .build()) + .build(); + Object extractedAnyValue = extractAnyValue(anyValue); + if (extractedAnyValue instanceof Map<?, ?> genericMap) { + Map<String, Object> resultMap = new HashMap<>(); + for (Map.Entry<?, ?> entry : genericMap.entrySet()) { + if (entry.getKey() instanceof String) { + resultMap.put((String) entry.getKey(), entry.getValue()); + } + } + return resultMap; + } else { + return new HashMap<>(); + } + } + + /** + * Extract body content from AnyValue. + */ + private Object extractBody(AnyValue body) { + return extractAnyValue(body); + } + + /** + * Extract value from OpenTelemetry AnyValue. + */ + private Object extractAnyValue(AnyValue anyValue) { + switch (anyValue.getValueCase()) { + case STRING_VALUE: + return anyValue.getStringValue(); + case BOOL_VALUE: + return anyValue.getBoolValue(); + case INT_VALUE: + return anyValue.getIntValue(); + case DOUBLE_VALUE: + return anyValue.getDoubleValue(); + case ARRAY_VALUE: + List<Object> arrayList = new ArrayList<>(); + for (AnyValue item : anyValue.getArrayValue().getValuesList()) { + arrayList.add(extractAnyValue(item)); + } + return arrayList; + case KVLIST_VALUE: + Map<String, Object> kvMap = new HashMap<>(); + for (KeyValue kv : anyValue.getKvlistValue().getValuesList()) { + kvMap.put(normalizeKey(kv.getKey()), extractAnyValue(kv.getValue())); + } + return kvMap; + case BYTES_VALUE: + return anyValue.getBytesValue().toByteArray(); + case VALUE_NOT_SET: + default: + return null; + } + } + + /** + * Normalize key by replacing dots and spaces with underscores. + * + * @param key the original key + * @return normalized key with dots and spaces replaced by underscores + */ + private String normalizeKey(String key) { + if (key == null) { + return null; + } + return key.replace(".", "_").replace(" ", "_"); + } + + /** + * Convert byte array to hex string. + */ + private String bytesToHex(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return null; + } + StringBuilder hexString = new StringBuilder(); + for (byte b : bytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + @Override + public String supportProtocol() { + return PROTOCOL_NAME; + } +} \ No newline at end of file diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogIngestionControllerTest.java b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogIngestionControllerTest.java new file mode 100644 index 0000000..cb6eb36 --- /dev/null +++ b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogIngestionControllerTest.java @@ -0,0 +1,117 @@ +/* + * 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.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +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 static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; + +import java.util.Arrays; +import java.util.List; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.log.service.LogProtocolAdapter; +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.http.MediaType; +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 LogIngestionController} + */ +@ExtendWith(MockitoExtension.class) +class LogIngestionControllerTest { + + private MockMvc mockMvc; + + @Mock + private LogProtocolAdapter vectorAdapter; + + private LogIngestionController logIngestionController; + + @BeforeEach + void setUp() { + List<LogProtocolAdapter> adapters = Arrays.asList(vectorAdapter); + this.logIngestionController = new LogIngestionController(adapters); + this.mockMvc = MockMvcBuilders.standaloneSetup(logIngestionController).build(); + } + + @Test + void testIngestLogWithKnownProtocol() throws Exception { + String logContent = "{\"message\":\"Test log message\"}"; + + when(vectorAdapter.supportProtocol()).thenReturn("vector"); + doNothing().when(vectorAdapter).ingest(anyString()); + + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/logs/ingest/vector") + .contentType(MediaType.APPLICATION_JSON) + .content(logContent) + ) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Add extern log success")) + .andReturn(); + } + + @Test + void testIngestLogWithUnsupportedProtocol() throws Exception { + String logContent = "{\"message\":\"Unsupported protocol log\"}"; + + when(vectorAdapter.supportProtocol()).thenReturn("vector"); + + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/logs/ingest/unsupported") + .contentType(MediaType.APPLICATION_JSON) + .content(logContent) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("Not support the unsupported protocol log")); + } + + @Test + void testIngestLogWithAdapterException() throws Exception { + String logContent = "{\"message\":\"Log message that will cause exception\"}"; + + when(vectorAdapter.supportProtocol()).thenReturn("vector"); + doThrow(new IllegalArgumentException("Invalid log format")).when(vectorAdapter).ingest(anyString()); + + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/logs/ingest/vector") + .contentType(MediaType.APPLICATION_JSON) + .content(logContent) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("Add extern log failed: Invalid log format")); + } +} diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogManagerControllerTest.java b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogManagerControllerTest.java new file mode 100644 index 0000000..8bd4939 --- /dev/null +++ b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogManagerControllerTest.java @@ -0,0 +1,101 @@ +/* + * 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.ArgumentMatchers.anyList; +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 static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; + +import java.util.Arrays; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataWriter; +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.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Unit test for {@link LogManagerController} + */ +@ExtendWith(MockitoExtension.class) +class LogManagerControllerTest { + + private MockMvc mockMvc; + + @Mock + private HistoryDataWriter historyDataWriter; + + private LogManagerController logManagerController; + + @BeforeEach + void setUp() { + this.logManagerController = new LogManagerController(historyDataWriter); + this.mockMvc = MockMvcBuilders.standaloneSetup(logManagerController).build(); + } + + @Test + void testBatchDeleteLogsSuccess() throws Exception { + when(historyDataWriter.batchDeleteLogs(anyList())).thenReturn(true); + + mockMvc.perform( + MockMvcRequestBuilders + .delete("/api/logs") + .param("timeUnixNanos", "1734005477630000000", "1734005477640000000") + ) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Logs deleted successfully")) + .andReturn(); + } + + @Test + void testBatchDeleteLogsFailure() throws Exception { + when(historyDataWriter.batchDeleteLogs(anyList())).thenReturn(false); + + mockMvc.perform( + MockMvcRequestBuilders + .delete("/api/logs") + .param("timeUnixNanos", "1734005477630000000", "1734005477640000000") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("Failed to delete logs")); + } + + @Test + void testBatchDeleteLogsWithSingleTimestamp() throws Exception { + when(historyDataWriter.batchDeleteLogs(Arrays.asList(1734005477630000000L))).thenReturn(true); + + mockMvc.perform( + MockMvcRequestBuilders + .delete("/api/logs") + .param("timeUnixNanos", "1734005477630000000") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Logs deleted successfully")); + } +} diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogQueryControllerTest.java b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogQueryControllerTest.java new file mode 100644 index 0000000..8e3da88 --- /dev/null +++ b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogQueryControllerTest.java @@ -0,0 +1,277 @@ +/* + * 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.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +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 static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.log.LogEntry; +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.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Unit test for {@link LogQueryController} + */ +@ExtendWith(MockitoExtension.class) +class LogQueryControllerTest { + + private MockMvc mockMvc; + + @Mock + private HistoryDataReader historyDataReader; + + private LogQueryController logQueryController; + + @BeforeEach + void setUp() { + this.logQueryController = new LogQueryController(historyDataReader); + this.mockMvc = MockMvcBuilders.standaloneSetup(logQueryController).build(); + } + + @Test + void testListLogsWithAllFilters() throws Exception { + // Mock data + LogEntry logEntry1 = LogEntry.builder() + .timeUnixNano(1734005477630000000L) + .severityNumber(9) + .severityText("INFO") + .body("Test log message 1") + .traceId("trace123") + .spanId("span456") + .attributes(new HashMap<>()) + .build(); + + LogEntry logEntry2 = LogEntry.builder() + .timeUnixNano(1734005477640000000L) + .severityNumber(17) + .severityText("ERROR") + .body("Test log message 2") + .traceId("trace123") + .spanId("span789") + .attributes(new HashMap<>()) + .build(); + + List<LogEntry> mockLogs = Arrays.asList(logEntry1, logEntry2); + + when(historyDataReader.countLogsByMultipleConditions(anyLong(), anyLong(), any(), + any(), any(), any(), any())).thenReturn(2L); + when(historyDataReader.queryLogsByMultipleConditionsWithPagination(anyLong(), anyLong(), + any(), any(), any(), any(), any(), anyInt(), anyInt())) + .thenReturn(mockLogs); + + mockMvc.perform( + MockMvcRequestBuilders + .get("/api/logs/list") + .param("start", "1734005477000") + .param("end", "1734005478000") + .param("traceId", "trace123") + .param("spanId", "span456") + .param("severityNumber", "9") + .param("severityText", "INFO") + .param("pageIndex", "0") + .param("pageSize", "20") + ) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(2)) + .andExpect(jsonPath("$.data.totalElements").value(2)) + .andExpect(jsonPath("$.data.size").value(20)) + .andExpect(jsonPath("$.data.number").value(0)); + } + + @Test + void testListLogsWithoutFilters() throws Exception { + List<LogEntry> mockLogs = Arrays.asList( + LogEntry.builder() + .timeUnixNano(1734005477630000000L) + .severityNumber(9) + .severityText("INFO") + .body("Test log message") + .attributes(new HashMap<>()) + .build() + ); + + when(historyDataReader.countLogsByMultipleConditions(any(), any(), any(), + any(), any(), any(), any())).thenReturn(1L); + when(historyDataReader.queryLogsByMultipleConditionsWithPagination(any(), any(), + any(), any(), any(), any(), any(), eq(0), eq(20))) + .thenReturn(mockLogs); + + mockMvc.perform( + MockMvcRequestBuilders + .get("/api/logs/list") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(1)); + } + + @Test + void testOverviewStatsWithMixedSeverityLogs() throws Exception { + // Create logs with different severity levels according to OpenTelemetry standard + List<LogEntry> mockLogs = Arrays.asList( + // TRACE (1-4) + LogEntry.builder().severityNumber(2).build(), + // DEBUG (5-8) + LogEntry.builder().severityNumber(6).build(), + // INFO (9-12) + LogEntry.builder().severityNumber(9).build(), + LogEntry.builder().severityNumber(10).build(), + // WARN (13-16) + LogEntry.builder().severityNumber(14).build(), + // ERROR (17-20) + LogEntry.builder().severityNumber(17).build(), + LogEntry.builder().severityNumber(18).build(), + // FATAL (21-24) + LogEntry.builder().severityNumber(21).build() + ); + + when(historyDataReader.queryLogsByMultipleConditions(any(), any(), any(), + any(), any(), any(), any())).thenReturn(mockLogs); + + mockMvc.perform( + MockMvcRequestBuilders + .get("/api/logs/stats/overview") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.totalCount").value(8)) + .andExpect(jsonPath("$.data.traceCount").value(1)) + .andExpect(jsonPath("$.data.debugCount").value(1)) + .andExpect(jsonPath("$.data.infoCount").value(2)) + .andExpect(jsonPath("$.data.warnCount").value(1)) + .andExpect(jsonPath("$.data.errorCount").value(2)) + .andExpect(jsonPath("$.data.fatalCount").value(1)); + } + + @Test + void testOverviewStatsWithTimeRange() throws Exception { + List<LogEntry> mockLogs = Arrays.asList( + LogEntry.builder().severityNumber(9).build(), + LogEntry.builder().severityNumber(17).build() + ); + + when(historyDataReader.queryLogsByMultipleConditions(eq(1734005477000L), eq(1734005478000L), + any(), any(), any(), any(), any())).thenReturn(mockLogs); + + mockMvc.perform( + MockMvcRequestBuilders + .get("/api/logs/stats/overview") + .param("start", "1734005477000") + .param("end", "1734005478000") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.totalCount").value(2)); + } + + @Test + void testTraceCoverageStats() throws Exception { + List<LogEntry> mockLogs = Arrays.asList( + // With both trace and span + LogEntry.builder().traceId("trace1").spanId("span1").build(), + LogEntry.builder().traceId("trace2").spanId("span2").build(), + // With trace only + LogEntry.builder().traceId("trace3").spanId("").build(), + // With span only + LogEntry.builder().traceId("").spanId("span4").build(), + // Without trace info + LogEntry.builder().traceId("").spanId("").build(), + LogEntry.builder().build() // null values + ); + + when(historyDataReader.queryLogsByMultipleConditions(any(), any(), any(), + any(), any(), any(), any())).thenReturn(mockLogs); + + mockMvc.perform( + MockMvcRequestBuilders + .get("/api/logs/stats/trace-coverage") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.traceCoverage.withTrace").value(3)) + .andExpect(jsonPath("$.data.traceCoverage.withoutTrace").value(3)) + .andExpect(jsonPath("$.data.traceCoverage.withSpan").value(3)) + .andExpect(jsonPath("$.data.traceCoverage.withBothTraceAndSpan").value(2)); + } + + @Test + void testTrendStats() throws Exception { + // Create logs with timestamps that fall into different hours + List<LogEntry> mockLogs = Arrays.asList( + // 2023-12-12 10:00 (1734005477630000000L nano = 1734005477630L ms) + LogEntry.builder().timeUnixNano(1734005477630000000L).build(), + // Same hour + LogEntry.builder().timeUnixNano(1734005477640000000L).build(), + // Next hour: 2023-12-12 11:00 (1734009077630000000L nano = 1734009077630L ms) + LogEntry.builder().timeUnixNano(1734009077630000000L).build() + ); + + when(historyDataReader.queryLogsByMultipleConditions(any(), any(), any(), + any(), any(), any(), any())).thenReturn(mockLogs); + + mockMvc.perform( + MockMvcRequestBuilders + .get("/api/logs/stats/trend") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.hourlyStats").isMap()); + } + + @Test + void testTrendStatsWithNullTimestamp() throws Exception { + List<LogEntry> mockLogs = Arrays.asList( + LogEntry.builder().timeUnixNano(1734005477630000000L).build(), + LogEntry.builder().timeUnixNano(null).build() // This should be filtered out + ); + + when(historyDataReader.queryLogsByMultipleConditions(any(), any(), any(), + any(), any(), any(), any())).thenReturn(mockLogs); + + mockMvc.perform( + MockMvcRequestBuilders + .get("/api/logs/stats/trend") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.hourlyStats").isMap()); + } + +} diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogSseControllerTest.java b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogSseControllerTest.java new file mode 100644 index 0000000..31e1eb3 --- /dev/null +++ b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogSseControllerTest.java @@ -0,0 +1,113 @@ +/* + * 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.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.status; + +import org.apache.hertzbeat.log.notice.LogSseFilterCriteria; +import org.apache.hertzbeat.log.notice.LogSseManager; + +import org.junit.jupiter.api.Assertions; +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.Captor; +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.setup.MockMvcBuilders; + +/** + * Unit tests for the {@link LogSseController}. + */ +@ExtendWith(MockitoExtension.class) +class LogSseControllerTest { + + private MockMvc mockMvc; + + @Mock + private LogSseManager emitterManager; + + @Captor + private ArgumentCaptor<LogSseFilterCriteria> filterCriteriaCaptor; + + @BeforeEach + void setUp() { + // Initialize the controller and MockMvc instance before each test + LogSseController logSseController = new LogSseController(emitterManager); + this.mockMvc = MockMvcBuilders.standaloneSetup(logSseController).build(); + } + + @Test + void testSubscribeWithoutFilters() throws Exception { + // When: A request is made to the subscribe endpoint without any parameters + mockMvc.perform(get("/api/logs/sse/subscribe") + .accept(MediaType.TEXT_EVENT_STREAM_VALUE)) + .andExpect(status().isOk()); + + // Then: The emitter manager is called with an empty filter criteria + verify(emitterManager).createEmitter(anyLong(), filterCriteriaCaptor.capture()); + LogSseFilterCriteria capturedCriteria = filterCriteriaCaptor.getValue(); + + Assertions.assertNull(capturedCriteria.getSeverityText()); + Assertions.assertNull(capturedCriteria.getSeverityNumber()); + Assertions.assertNull(capturedCriteria.getTraceId()); + Assertions.assertNull(capturedCriteria.getSpanId()); + } + + @Test + void testSubscribeWithMultipleFilters() throws Exception { + // Given: Multiple filter parameters + String severityText = "ERROR"; + String severityNumber = "17"; + String traceId = "abcdef1234567890abcdef1234567890"; + String spanId = "abcdef1234567890"; + + // When: A request is made with all filter parameters + mockMvc.perform(get("/api/logs/sse/subscribe") + .param("severityText", severityText) + .param("severityNumber", severityNumber) + .param("traceId", traceId) + .param("spanId", spanId) + .accept(MediaType.TEXT_EVENT_STREAM_VALUE)) + .andExpect(status().isOk()); + + // Then: The emitter manager is called with a criteria object containing all filter values + verify(emitterManager).createEmitter(anyLong(), filterCriteriaCaptor.capture()); + LogSseFilterCriteria capturedCriteria = filterCriteriaCaptor.getValue(); + + Assertions.assertEquals(capturedCriteria.getSeverityText(), severityText); + Assertions.assertEquals(capturedCriteria.getSeverityNumber(), Integer.parseInt(severityNumber)); + Assertions.assertEquals(capturedCriteria.getTraceId(), traceId); + Assertions.assertEquals(capturedCriteria.getSpanId(), spanId); + } + + @Test + void testSubscribeWithInvalidSeverityNumber() throws Exception { + // When: A request is made with a non-integer value for severityNumber + mockMvc.perform(get("/api/logs/sse/subscribe") + .param("severityNumber", "not-a-number") + .accept(MediaType.TEXT_EVENT_STREAM_VALUE)) + .andExpect(status().is4xxClientError()); + } +} \ No newline at end of file diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/OtlpLogControllerTest.java b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/OtlpLogControllerTest.java new file mode 100644 index 0000000..d2e67bc --- /dev/null +++ b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/OtlpLogControllerTest.java @@ -0,0 +1,131 @@ +/* + * 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.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; + +import org.apache.hertzbeat.log.service.impl.OtlpLogProtocolAdapter; +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.http.MediaType; +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 OtlpLogController} + */ +@ExtendWith(MockitoExtension.class) +class OtlpLogControllerTest { + + private static final String CONTENT_TYPE_PROTOBUF = "application/x-protobuf"; + + private MockMvc mockMvc; + + @Mock + private OtlpLogProtocolAdapter otlpLogProtocolAdapter; + + private OtlpLogController otlpLogController; + + @BeforeEach + void setUp() { + this.otlpLogController = new OtlpLogController(otlpLogProtocolAdapter); + this.mockMvc = MockMvcBuilders.standaloneSetup(otlpLogController).build(); + } + + @Test + void testIngestJsonLogsSuccess() throws Exception { + String jsonContent = "{\"resourceLogs\":[]}"; + + doNothing().when(otlpLogProtocolAdapter).ingest(anyString()); + + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/logs/otlp/v1/logs") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonContent) + ) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andReturn(); + } + + @Test + void testIngestJsonLogsFailure() throws Exception { + String jsonContent = "{\"invalid\":\"content\"}"; + + doThrow(new IllegalArgumentException("Invalid OTLP JSON log content")) + .when(otlpLogProtocolAdapter).ingest(anyString()); + + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/logs/otlp/v1/logs") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonContent) + ) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andReturn(); + } + + @Test + void testIngestBinaryLogsSuccess() throws Exception { + byte[] binaryContent = new byte[]{0x0a, 0x0b, 0x0c}; + + doNothing().when(otlpLogProtocolAdapter).ingestBinary(any(byte[].class)); + + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/logs/otlp/v1/logs") + .contentType(CONTENT_TYPE_PROTOBUF) + .content(binaryContent) + ) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(CONTENT_TYPE_PROTOBUF)) + .andReturn(); + } + + @Test + void testIngestBinaryLogsFailure() throws Exception { + byte[] binaryContent = new byte[]{0x0a, 0x0b, 0x0c}; + + doThrow(new IllegalArgumentException("Invalid OTLP binary log content")) + .when(otlpLogProtocolAdapter).ingestBinary(any(byte[].class)); + + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/logs/otlp/v1/logs") + .contentType(CONTENT_TYPE_PROTOBUF) + .content(binaryContent) + ) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(CONTENT_TYPE_PROTOBUF)) + .andReturn(); + } +} diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/notice/LogSseFilterCriteriaTest.java b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/notice/LogSseFilterCriteriaTest.java new file mode 100644 index 0000000..b06284d --- /dev/null +++ b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/notice/LogSseFilterCriteriaTest.java @@ -0,0 +1,227 @@ +/* + * 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.notice; + +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/** + * Unit tests for LogSseFilterCriteria. + */ +class LogSseFilterCriteriaTest { + + private LogEntry testLogEntry; + private LogSseFilterCriteria filterCriteria; + + @BeforeEach + void setUp() { + // Create test LogEntry + testLogEntry = LogEntry.builder() + .severityNumber(9) + .severityText("INFO") + .traceId("1234567890abcdef1234567890abcdef") + .spanId("1234567890abcdef") + .body("Test log message") + .timeUnixNano(System.currentTimeMillis() * 1_000_000L) + .build(); + + filterCriteria = new LogSseFilterCriteria(); + } + + @Test + void testMatchesWithNoFilters() { + // Should match all logs when no filters are set + assertTrue(filterCriteria.matches(testLogEntry)); + } + + @Test + void testMatchesWithSeverityTextFilter() { + // Test severity text filter - match + filterCriteria.setSeverityText("INFO"); + assertTrue(filterCriteria.matches(testLogEntry)); + + // Test severity text filter - no match + filterCriteria.setSeverityText("ERROR"); + assertFalse(filterCriteria.matches(testLogEntry)); + + // Test severity text filter - case insensitive + filterCriteria.setSeverityText("info"); + assertTrue(filterCriteria.matches(testLogEntry)); + } + + @Test + void testMatchesWithSeverityNumberFilter() { + // Test severity number filter - match + filterCriteria.setSeverityNumber(9); + assertTrue(filterCriteria.matches(testLogEntry)); + + // Test severity number filter - no match + filterCriteria.setSeverityNumber(1); + assertFalse(filterCriteria.matches(testLogEntry)); + + // Test severity number filter - null value + filterCriteria.setSeverityNumber(null); + assertTrue(filterCriteria.matches(testLogEntry)); + } + + @Test + void testMatchesWithTraceIdFilter() { + // Test Trace ID filter - match + filterCriteria.setTraceId("1234567890abcdef1234567890abcdef"); + assertTrue(filterCriteria.matches(testLogEntry)); + + // Test Trace ID filter - no match + filterCriteria.setTraceId("abcdef1234567890abcdef1234567890"); + assertFalse(filterCriteria.matches(testLogEntry)); + + // Test Trace ID filter - case insensitive + filterCriteria.setTraceId("1234567890ABCDEF1234567890ABCDEF"); + assertTrue(filterCriteria.matches(testLogEntry)); + } + + @Test + void testMatchesWithSpanIdFilter() { + // Test Span ID filter - match + filterCriteria.setSpanId("1234567890abcdef"); + assertTrue(filterCriteria.matches(testLogEntry)); + + // Test Span ID filter - no match + filterCriteria.setSpanId("abcdef1234567890"); + assertFalse(filterCriteria.matches(testLogEntry)); + + // Test Span ID filter - case insensitive + filterCriteria.setSpanId("1234567890ABCDEF"); + assertTrue(filterCriteria.matches(testLogEntry)); + } + + @Test + void testMatchesWithLogContentFilter() { + // Test log content filter - match + filterCriteria.setLogContent("Test log"); + assertTrue(filterCriteria.matches(testLogEntry)); + + // Test log content filter - no match + filterCriteria.setLogContent("Error message"); + assertFalse(filterCriteria.matches(testLogEntry)); + + // Test log content filter - case insensitive + filterCriteria.setLogContent("test log"); + assertTrue(filterCriteria.matches(testLogEntry)); + + // Test log content filter - partial match + filterCriteria.setLogContent("message"); + assertTrue(filterCriteria.matches(testLogEntry)); + + // Test log content filter with null body + LogEntry nullBodyLog = LogEntry.builder() + .severityNumber(9) + .severityText("INFO") + .body(null) + .build(); + filterCriteria.setLogContent("test"); + assertFalse(filterCriteria.matches(nullBodyLog)); + } + + @Test + void testMatchesWithMultipleFilters() { + // Test multiple filter combinations - all match + filterCriteria.setSeverityText("INFO"); + filterCriteria.setSeverityNumber(9); + filterCriteria.setTraceId("1234567890abcdef1234567890abcdef"); + filterCriteria.setSpanId("1234567890abcdef"); + assertTrue(filterCriteria.matches(testLogEntry)); + + // Test multiple filter combinations - partial no match + filterCriteria.setSeverityText("ERROR"); + assertFalse(filterCriteria.matches(testLogEntry)); + } + + @Test + void testMatchesWithEmptyStringFilters() { + // Test empty string filters + filterCriteria.setSeverityText(""); + filterCriteria.setTraceId(""); + filterCriteria.setSpanId(""); + assertTrue(filterCriteria.matches(testLogEntry)); + } + + @Test + void testMatchesWithLogEntryHavingNullValues() { + // Create log entry with null values + LogEntry logWithNulls = LogEntry.builder() + .severityNumber(null) + .severityText(null) + .traceId(null) + .spanId(null) + .body("Test log with nulls") + .build(); + + // Set filter criteria + filterCriteria.setSeverityNumber(9); + filterCriteria.setSeverityText("INFO"); + filterCriteria.setTraceId("1234567890abcdef1234567890abcdef"); + filterCriteria.setSpanId("1234567890abcdef"); + + // Should not match because log entry values are null + assertFalse(filterCriteria.matches(logWithNulls)); + } + + @Test + void testConstructorWithAllParameters() { + // Test constructor with all parameters + LogSseFilterCriteria criteria = new LogSseFilterCriteria( + 9, "INFO", null, "1234567890abcdef1234567890abcdef", "1234567890abcdef" + ); + + assertEquals(9, criteria.getSeverityNumber()); + assertEquals("INFO", criteria.getSeverityText()); + assertEquals(null, criteria.getLogContent()); + assertEquals("1234567890abcdef1234567890abcdef", criteria.getTraceId()); + assertEquals("1234567890abcdef", criteria.getSpanId()); + + // Test matching + assertTrue(criteria.matches(testLogEntry)); + } + + @Test + void testNoArgsConstructorAndSetters() { + // Test no-args constructor and setter methods + LogSseFilterCriteria criteria = new LogSseFilterCriteria(); + + criteria.setSeverityNumber(9); + criteria.setSeverityText("INFO"); + criteria.setLogContent("Test log"); + criteria.setTraceId("1234567890abcdef1234567890abcdef"); + criteria.setSpanId("1234567890abcdef"); + + assertEquals(9, criteria.getSeverityNumber()); + assertEquals("INFO", criteria.getSeverityText()); + assertEquals("Test log", criteria.getLogContent()); + assertEquals("1234567890abcdef1234567890abcdef", criteria.getTraceId()); + assertEquals("1234567890abcdef", criteria.getSpanId()); + + // Test matching + assertTrue(criteria.matches(testLogEntry)); + } +} diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/notice/LogSseManagerTest.java b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/notice/LogSseManagerTest.java new file mode 100644 index 0000000..9ddc3b7 --- /dev/null +++ b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/notice/LogSseManagerTest.java @@ -0,0 +1,224 @@ +/* + * 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.notice; + +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.awaitility.Awaitility.await; +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.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * Unit tests for {@link LogSseManager}. + */ +class LogSseManagerTest { + + private LogSseManager logSseManager; + private static final Long CLIENT_ID = 1L; + + @BeforeEach + void setUp() { + logSseManager = new LogSseManager(); + } + + @AfterEach + void tearDown() { + logSseManager.shutdown(); + } + + @Test + void shouldCreateAndStoreEmitter() { + // When: Creating a new emitter for a client + SseEmitter emitter = logSseManager.createEmitter(CLIENT_ID, new LogSseFilterCriteria()); + + // Then: The emitter should be created and stored + assertNotNull(emitter); + assertEquals(Long.MAX_VALUE, emitter.getTimeout()); + assertTrue(logSseManager.getEmitters().containsKey(CLIENT_ID)); + } + + @Test + void shouldBroadcastLogWhenFilterMatches() throws IOException { + // Given: A client with a filter for "INFO" logs + LogSseFilterCriteria filters = new LogSseFilterCriteria(); + filters.setSeverityText("INFO"); + SseEmitter mockEmitter = mock(SseEmitter.class); + subscribeClient(CLIENT_ID, filters, mockEmitter); + + LogEntry infoLog = createLogEntry("INFO", "An informational message"); + + // When: An "INFO" log is broadcast + logSseManager.broadcast(infoLog); + + // Then: The log should be sent to the client (wait for batch processing) + await().atMost(500, TimeUnit.MILLISECONDS).untilAsserted(() -> + verify(mockEmitter, atLeastOnce()).send(any(SseEmitter.SseEventBuilder.class)) + ); + } + + @Test + void shouldSendBatchOnVirtualThread() throws IOException, InterruptedException { + SseEmitter mockEmitter = mock(SseEmitter.class); + AtomicBoolean virtualThread = new AtomicBoolean(false); + CountDownLatch latch = new CountDownLatch(1); + doAnswer(invocation -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + return null; + }).when(mockEmitter).send(any(SseEmitter.SseEventBuilder.class)); + subscribeClient(CLIENT_ID, null, mockEmitter); + + logSseManager.broadcast(createLogEntry("INFO", "virtual-thread-send")); + + assertTrue(latch.await(1, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void shouldNotBroadcastLogWhenFilterDoesNotMatch() throws IOException, InterruptedException { + // Given: A client with a filter for "ERROR" logs + LogSseFilterCriteria filters = new LogSseFilterCriteria(); + filters.setSeverityText("ERROR"); + SseEmitter mockEmitter = mock(SseEmitter.class); + subscribeClient(CLIENT_ID, filters, mockEmitter); + + LogEntry infoLog = createLogEntry("INFO", "An informational message"); + + // When: An "INFO" log is broadcast + logSseManager.broadcast(infoLog); + + // Wait for batch processing + Thread.sleep(300); + + // Then: The log should NOT be sent to the client + verify(mockEmitter, never()).send(any(SseEmitter.SseEventBuilder.class)); + } + + @Test + void shouldBroadcastLogWhenSubscriberHasNoFilters() throws IOException { + // Given: A client subscribed with no filters (null) + SseEmitter mockEmitter = mock(SseEmitter.class); + subscribeClient(CLIENT_ID, null, mockEmitter); + + LogEntry anyLog = createLogEntry("DEBUG", "A debug message"); + + // When: Any log is broadcast + logSseManager.broadcast(anyLog); + + // Then: The log should be sent to the client (wait for batch processing) + await().atMost(500, TimeUnit.MILLISECONDS).untilAsserted(() -> + verify(mockEmitter, atLeastOnce()).send(any(SseEmitter.SseEventBuilder.class)) + ); + } + + @Test + void shouldBroadcastOnlyToMatchingSubscribers() throws IOException, InterruptedException { + // Given: Two clients with different filters + LogSseFilterCriteria infoFilter = new LogSseFilterCriteria(); + infoFilter.setSeverityText("INFO"); + SseEmitter infoEmitter = mock(SseEmitter.class); + subscribeClient(1L, infoFilter, infoEmitter); + + LogSseFilterCriteria errorFilter = new LogSseFilterCriteria(); + errorFilter.setSeverityText("ERROR"); + SseEmitter errorEmitter = mock(SseEmitter.class); + subscribeClient(2L, errorFilter, errorEmitter); + + LogEntry infoLog = createLogEntry("INFO", "An informational message"); + + // When: An "INFO" log is broadcast + logSseManager.broadcast(infoLog); + + // Wait for batch processing + await().atMost(500, TimeUnit.MILLISECONDS).untilAsserted(() -> + verify(infoEmitter, atLeastOnce()).send(any(SseEmitter.SseEventBuilder.class)) + ); + + // Then: The log is sent only to the client subscribed to "INFO" logs + verify(errorEmitter, never()).send(any(SseEmitter.SseEventBuilder.class)); + } + + @Test + void shouldRemoveEmitterWhenBroadcastFails() throws IOException { + // Given: A client whose emitter will throw an exception on send + SseEmitter mockEmitter = mock(SseEmitter.class); + doThrow(new IOException("Connection closed")).when(mockEmitter).send(any(SseEmitter.SseEventBuilder.class)); + subscribeClient(CLIENT_ID, null, mockEmitter); + assertTrue(logSseManager.getEmitters().containsKey(CLIENT_ID)); + + LogEntry log = createLogEntry("ERROR", "An error occurred"); + + // When: A log is broadcast, causing an exception + logSseManager.broadcast(log); + + // Then: The failing emitter should be completed and removed + await().atMost(500, TimeUnit.MILLISECONDS).untilAsserted(() -> { + verify(mockEmitter).complete(); + assertFalse(logSseManager.getEmitters().containsKey(CLIENT_ID)); + }); + } + + @Test + void shouldDropLogsWhenQueueSizeLimitReached() { + for (int i = 0; i < 10_001; i++) { + logSseManager.broadcast(createLogEntry("INFO", "log-" + i)); + } + + assertEquals(10_000, logSseManager.getQueueSize()); + assertEquals(10_000, logSseManager.getLogQueue().size()); + } + + /** + * Helper method to create a subscriber and inject a mock emitter for testing + */ + private void subscribeClient(Long clientId, LogSseFilterCriteria filters, SseEmitter mockEmitter) { + logSseManager.createEmitter(clientId, filters); + LogSseManager.SseSubscriber subscriber = logSseManager.getEmitters().get(clientId); + subscriber.setEmitter(mockEmitter); + } + + /** + * Helper method to create LogEntry instances + */ + private LogEntry createLogEntry(String severityText, String body) { + return LogEntry.builder() + .severityText(severityText) + .body(body) + .build(); + } +} diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/service/impl/OtlpLogProtocolAdapterTest.java b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/service/impl/OtlpLogProtocolAdapterTest.java new file mode 100644 index 0000000..0a57015 --- /dev/null +++ b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/service/impl/OtlpLogProtocolAdapterTest.java @@ -0,0 +1,387 @@ +/* + * 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.service.impl; + +import com.google.protobuf.ByteString; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.logs.v1.LogRecord; +import io.opentelemetry.proto.logs.v1.ResourceLogs; +import io.opentelemetry.proto.logs.v1.ScopeLogs; +import io.opentelemetry.proto.resource.v1.Resource; +import io.opentelemetry.proto.common.v1.InstrumentationScope; +import org.apache.hertzbeat.common.entity.log.LogEntry; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.log.notice.LogSseManager; +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 java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.anyList; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Unit tests for OtlpLogProtocolAdapter. + */ +@ExtendWith(MockitoExtension.class) +class OtlpLogProtocolAdapterTest { + + @Mock + private CommonDataQueue commonDataQueue; + + @Mock + private LogSseManager logSseManager; + + private OtlpLogProtocolAdapter adapter; + + @BeforeEach + void setUp() { + adapter = new OtlpLogProtocolAdapter(commonDataQueue, logSseManager); + } + + @Test + void testIngestWithNullContent() { + adapter.ingest(null); + verifyNoInteractions(commonDataQueue, logSseManager); + } + + @Test + void testIngestWithEmptyContent() { + adapter.ingest(""); + verifyNoInteractions(commonDataQueue, logSseManager); + } + + @Test + void testIngestBinaryWithNullContent() { + adapter.ingestBinary(null); + verifyNoInteractions(commonDataQueue, logSseManager); + } + + @Test + void testIngestBinaryWithEmptyContent() { + adapter.ingestBinary(new byte[0]); + verifyNoInteractions(commonDataQueue, logSseManager); + } + + @Test + void testIngestBinaryWithValidOtlpLogData() throws Exception { + byte[] binaryPayload = createValidOtlpLogBinaryPayload(); + + adapter.ingestBinary(binaryPayload); + + ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class); + verify(commonDataQueue, times(1)).sendLogEntryToStorageBatch(listCaptor.capture()); + verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(anyList()); + verify(logSseManager, times(1)).broadcast(any(LogEntry.class)); + + List<LogEntry> capturedList = listCaptor.getValue(); + assertNotNull(capturedList); + assertEquals(1, capturedList.size()); + + LogEntry capturedEntry = capturedList.get(0); + assertEquals("binary-test-service", capturedEntry.getResource().get("service_name")); + assertEquals("binary log message", capturedEntry.getBody()); + assertEquals("INFO", capturedEntry.getSeverityText()); + } + + @Test + void testIngestBinaryWithInvalidContent() { + byte[] invalidBinary = "not a valid protobuf".getBytes(); + + assertThrows(IllegalArgumentException.class, () -> adapter.ingestBinary(invalidBinary)); + verifyNoInteractions(commonDataQueue, logSseManager); + } + + @Test + void testIngestWithValidOtlpLogData() throws Exception { + String otlpPayload = createValidOtlpLogPayload(); + + adapter.ingest(otlpPayload); + + ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class); + verify(commonDataQueue, times(1)).sendLogEntryToStorageBatch(listCaptor.capture()); + verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(anyList()); + verify(logSseManager, times(1)).broadcast(any(LogEntry.class)); + + List<LogEntry> capturedList = listCaptor.getValue(); + assertNotNull(capturedList); + assertEquals(1, capturedList.size()); + + LogEntry capturedEntry = capturedList.get(0); + assertEquals("test-service", capturedEntry.getResource().get("service_name")); + assertEquals("test-version", capturedEntry.getResource().get("service_version")); + assertEquals("test-scope", capturedEntry.getInstrumentationScope().getName()); + assertEquals("1.0.0", capturedEntry.getInstrumentationScope().getVersion()); + assertEquals("test log message", capturedEntry.getBody()); + assertEquals("INFO", capturedEntry.getSeverityText()); + assertEquals(9, capturedEntry.getSeverityNumber()); + } + + @Test + void testIngestWithMultipleLogRecords() throws Exception { + String otlpPayload = createOtlpPayloadWithMultipleLogs(); + + adapter.ingest(otlpPayload); + + ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class); + verify(commonDataQueue, times(1)).sendLogEntryToStorageBatch(listCaptor.capture()); + verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(anyList()); + verify(logSseManager, times(2)).broadcast(any(LogEntry.class)); + + List<LogEntry> capturedList = listCaptor.getValue(); + assertEquals(2, capturedList.size()); + } + + @Test + void testIngestWithComplexAttributes() throws Exception { + String otlpPayload = createOtlpPayloadWithComplexAttributes(); + + adapter.ingest(otlpPayload); + + ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class); + verify(commonDataQueue, times(1)).sendLogEntryToStorageBatch(listCaptor.capture()); + verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(anyList()); + verify(logSseManager, times(1)).broadcast(any(LogEntry.class)); + + List<LogEntry> capturedList = listCaptor.getValue(); + assertNotNull(capturedList); + assertEquals(1, capturedList.size()); + + LogEntry capturedEntry = capturedList.get(0); + Map<String, Object> attributes = capturedEntry.getAttributes(); + assertEquals("string_value", attributes.get("string_attr")); + assertEquals(true, attributes.get("bool_attr")); + assertEquals(42L, attributes.get("int_attr")); + assertEquals(3.14, attributes.get("double_attr")); + + List<Object> arrayAttr = (List<Object>) attributes.get("array_attr"); + assertNotNull(arrayAttr); + assertEquals(3, arrayAttr.size()); + assertEquals("item1", arrayAttr.get(0)); + assertEquals("item2", arrayAttr.get(1)); + assertEquals("item3", arrayAttr.get(2)); + } + + @Test + void testIngestWithTraceAndSpanIds() throws Exception { + String otlpPayload = createOtlpPayloadWithTraceSpanIds(); + + adapter.ingest(otlpPayload); + + ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class); + verify(commonDataQueue, times(1)).sendLogEntryToStorageBatch(listCaptor.capture()); + verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(anyList()); + verify(logSseManager, times(1)).broadcast(any(LogEntry.class)); + + List<LogEntry> capturedList = listCaptor.getValue(); + assertEquals(1, capturedList.size()); + + LogEntry capturedEntry = capturedList.get(0); + assertEquals("1234567890abcdef1234567890abcdef", capturedEntry.getTraceId()); + assertEquals("1234567890abcdef", capturedEntry.getSpanId()); + assertEquals(1, capturedEntry.getTraceFlags()); + } + + @Test + void testIngestWithInvalidJsonContent() { + String invalidJson = "{ invalid json content }"; + + assertThrows(IllegalArgumentException.class, () -> adapter.ingest(invalidJson)); + verifyNoInteractions(commonDataQueue, logSseManager); + } + + @Test + void testIngestWithEmptyResourceLogs() throws Exception { + String otlpPayload = createEmptyResourceLogsPayload(); + + adapter.ingest(otlpPayload); + + ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class); + verify(commonDataQueue, times(1)).sendLogEntryToStorageBatch(listCaptor.capture()); + verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(anyList()); + + List<LogEntry> capturedList = listCaptor.getValue(); + assertNotNull(capturedList); + assertEquals(0, capturedList.size()); + + verifyNoInteractions(logSseManager); + } + + private String createValidOtlpLogPayload() throws Exception { + ExportLogsServiceRequest request = ExportLogsServiceRequest.newBuilder() + .addResourceLogs(ResourceLogs.newBuilder() + .setResource(Resource.newBuilder() + .addAttributes(KeyValue.newBuilder() + .setKey("service.name") + .setValue(AnyValue.newBuilder().setStringValue("test-service").build()) + .build()) + .addAttributes(KeyValue.newBuilder() + .setKey("service.version") + .setValue(AnyValue.newBuilder().setStringValue("test-version").build()) + .build()) + .build()) + .addScopeLogs(ScopeLogs.newBuilder() + .setScope(InstrumentationScope.newBuilder() + .setName("test-scope") + .setVersion("1.0.0") + .build()) + .addLogRecords(LogRecord.newBuilder() + .setTimeUnixNano(System.currentTimeMillis() * 1_000_000) + .setObservedTimeUnixNano(System.currentTimeMillis() * 1_000_000) + .setSeverityNumberValue(9) + .setSeverityText("INFO") + .setBody(AnyValue.newBuilder().setStringValue("test log message").build()) + .build()) + .build()) + .build()) + .build(); + + return JsonFormat.printer().print(request); + } + + private String createOtlpPayloadWithMultipleLogs() throws Exception { + ExportLogsServiceRequest request = ExportLogsServiceRequest.newBuilder() + .addResourceLogs(ResourceLogs.newBuilder() + .setResource(Resource.newBuilder().build()) + .addScopeLogs(ScopeLogs.newBuilder() + .setScope(InstrumentationScope.newBuilder().build()) + .addLogRecords(LogRecord.newBuilder() + .setTimeUnixNano(System.currentTimeMillis() * 1_000_000) + .setBody(AnyValue.newBuilder().setStringValue("first log").build()) + .build()) + .addLogRecords(LogRecord.newBuilder() + .setTimeUnixNano(System.currentTimeMillis() * 1_000_000) + .setBody(AnyValue.newBuilder().setStringValue("second log").build()) + .build()) + .build()) + .build()) + .build(); + + return JsonFormat.printer().print(request); + } + + private String createOtlpPayloadWithComplexAttributes() throws Exception { + ExportLogsServiceRequest request = ExportLogsServiceRequest.newBuilder() + .addResourceLogs(ResourceLogs.newBuilder() + .setResource(Resource.newBuilder().build()) + .addScopeLogs(ScopeLogs.newBuilder() + .setScope(InstrumentationScope.newBuilder().build()) + .addLogRecords(LogRecord.newBuilder() + .setTimeUnixNano(System.currentTimeMillis() * 1_000_000) + .setBody(AnyValue.newBuilder().setStringValue("complex attributes test").build()) + .addAttributes(KeyValue.newBuilder() + .setKey("string.attr") + .setValue(AnyValue.newBuilder().setStringValue("string_value").build()) + .build()) + .addAttributes(KeyValue.newBuilder() + .setKey("bool.attr") + .setValue(AnyValue.newBuilder().setBoolValue(true).build()) + .build()) + .addAttributes(KeyValue.newBuilder() + .setKey("int.attr") + .setValue(AnyValue.newBuilder().setIntValue(42).build()) + .build()) + .addAttributes(KeyValue.newBuilder() + .setKey("double.attr") + .setValue(AnyValue.newBuilder().setDoubleValue(3.14).build()) + .build()) + .addAttributes(KeyValue.newBuilder() + .setKey("array.attr") + .setValue(AnyValue.newBuilder() + .setArrayValue(io.opentelemetry.proto.common.v1.ArrayValue.newBuilder() + .addValues(AnyValue.newBuilder().setStringValue("item1").build()) + .addValues(AnyValue.newBuilder().setStringValue("item2").build()) + .addValues(AnyValue.newBuilder().setStringValue("item3").build()) + .build()) + .build()) + .build()) + .build()) + .build()) + .build()) + .build(); + + return JsonFormat.printer().print(request); + } + + private String createOtlpPayloadWithTraceSpanIds() throws Exception { + ExportLogsServiceRequest request = ExportLogsServiceRequest.newBuilder() + .addResourceLogs(ResourceLogs.newBuilder() + .setResource(Resource.newBuilder().build()) + .addScopeLogs(ScopeLogs.newBuilder() + .setScope(InstrumentationScope.newBuilder().build()) + .addLogRecords(LogRecord.newBuilder() + .setTimeUnixNano(System.currentTimeMillis() * 1_000_000) + .setBody(AnyValue.newBuilder().setStringValue("trace test").build()) + .setTraceId(ByteString.fromHex("1234567890abcdef1234567890abcdef")) + .setSpanId(ByteString.fromHex("1234567890abcdef")) + .setFlags(1) + .build()) + .build()) + .build()) + .build(); + + return JsonFormat.printer().print(request); + } + + private String createEmptyResourceLogsPayload() throws Exception { + ExportLogsServiceRequest request = ExportLogsServiceRequest.newBuilder().build(); + return JsonFormat.printer().print(request); + } + + private byte[] createValidOtlpLogBinaryPayload() { + ExportLogsServiceRequest request = ExportLogsServiceRequest.newBuilder() + .addResourceLogs(ResourceLogs.newBuilder() + .setResource(Resource.newBuilder() + .addAttributes(KeyValue.newBuilder() + .setKey("service.name") + .setValue(AnyValue.newBuilder().setStringValue("binary-test-service").build()) + .build()) + .build()) + .addScopeLogs(ScopeLogs.newBuilder() + .setScope(InstrumentationScope.newBuilder() + .setName("binary-test-scope") + .setVersion("1.0.0") + .build()) + .addLogRecords(LogRecord.newBuilder() + .setTimeUnixNano(System.currentTimeMillis() * 1_000_000) + .setObservedTimeUnixNano(System.currentTimeMillis() * 1_000_000) + .setSeverityNumberValue(9) + .setSeverityText("INFO") + .setBody(AnyValue.newBuilder().setStringValue("binary log message").build()) + .build()) + .build()) + .build()) + .build(); + + return request.toByteArray(); + } +} diff --git a/hertzbeat-manager/pom.xml b/hertzbeat-manager/pom.xml new file mode 100644 index 0000000..57994ea --- /dev/null +++ b/hertzbeat-manager/pom.xml @@ -0,0 +1,221 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>hertzbeat</artifactId> + <groupId>org.apache.hertzbeat</groupId> + <version>2.0-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <artifactId>hertzbeat-manager</artifactId> + <name>${project.artifactId}</name> + <packaging>jar</packaging> + + + <dependencies> + <!-- common-spring (includes common-core) --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-common-spring</artifactId> + <exclusions> + <exclusion> + <artifactId>commons-logging</artifactId> + <groupId>commons-logging</groupId> + </exclusion> + </exclusions> + </dependency> + <!-- base --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-base</artifactId> + </dependency> + <!-- data warehouse --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-warehouse</artifactId> + </dependency> + <!-- alerter --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-alerter</artifactId> + </dependency> + <!-- collector --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-collector-collector</artifactId> + <exclusions> + <exclusion> + <artifactId>commons-logging</artifactId> + <groupId>commons-logging</groupId> + </exclusion> + </exclusions> + </dependency> + <!-- remoting --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-remoting</artifactId> + </dependency> + <!-- push --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-push</artifactId> + </dependency> + <!-- plugin --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-plugin</artifactId> + </dependency> + <!-- grafana --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-grafana</artifactId> + </dependency> + <!-- log otel --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-otel</artifactId> + </dependency> + <!-- log --> + <dependency> + <groupId>org.apache.hertzbeat</groupId> + <artifactId>hertzbeat-log</artifactId> + </dependency> + <!-- spring --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-configuration-processor</artifactId> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + <!--thymeleaf--> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-thymeleaf</artifactId> + </dependency> + + <!--webflux--> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-webflux</artifactId> + </dependency> + + <!-- freemarker --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-freemarker</artifactId> + </dependency> + <dependency> + <groupId>net.sourceforge.nekohtml</groupId> + <artifactId>nekohtml</artifactId> + <version>${nekohtml.version}</version> + </dependency> + <!-- data jdbc --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-jdbc</artifactId> + </dependency> + <!-- database migration --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-flyway</artifactId> + </dependency> + <dependency> + <groupId>org.flywaydb</groupId> + <artifactId>flyway-mysql</artifactId> + </dependency> + <dependency> + <groupId>org.flywaydb</groupId> + <artifactId>flyway-database-postgresql</artifactId> + </dependency> + <!-- mysql --> + <dependency> + <groupId>com.mysql</groupId> + <artifactId>mysql-connector-j</artifactId> + <scope>provided</scope> + </dependency> + <!-- email --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-mail</artifactId> + </dependency> + <!-- swagger --> + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + </dependency> + <!-- yaml file load --> + <dependency> + <groupId>org.yaml</groupId> + <artifactId>snakeyaml</artifactId> + </dependency> + <!--sureness--> + <dependency> + <groupId>com.usthe.sureness</groupId> + <artifactId>spring-boot3-starter-sureness</artifactId> + </dependency> + <!-- okhttp --> + <dependency> + <groupId>com.squareup.okhttp3</groupId> + <artifactId>okhttp</artifactId> + </dependency> + <!-- h2 database--> + <dependency> + <groupId>com.h2database</groupId> + <artifactId>h2</artifactId> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>cn.afterturn</groupId> + <artifactId>easypoi-spring-boot-starter</artifactId> + <version>${easy-poi.version}</version> + </dependency> + <dependency> + <groupId>com.huaweicloud</groupId> + <artifactId>esdk-obs-java</artifactId> + <version>${huawei.obs.version}</version> + <exclusions> + <exclusion> + <groupId>org.openeuler</groupId> + <artifactId>bgmprovider</artifactId> + </exclusion> + </exclusions> + </dependency> + + <dependency> + <groupId>org.apache.arrow</groupId> + <artifactId>arrow-vector</artifactId> + </dependency> + <dependency> + <groupId>org.apache.arrow</groupId> + <artifactId>arrow-memory-netty</artifactId> + </dependency> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + </dependency> + </dependencies> + +</project> diff --git a/hertzbeat-manager/src/main/java/com/usthe/sureness/configuration/SurenessJakartaServletFilter.java b/hertzbeat-manager/src/main/java/com/usthe/sureness/configuration/SurenessJakartaServletFilter.java new file mode 100644 index 0000000..008fe74 --- /dev/null +++ b/hertzbeat-manager/src/main/java/com/usthe/sureness/configuration/SurenessJakartaServletFilter.java @@ -0,0 +1,147 @@ +/* + * 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 com.usthe.sureness.configuration; + +import com.usthe.sureness.mgt.SecurityManager; +import com.usthe.sureness.processor.exception.DisabledAccountException; +import com.usthe.sureness.processor.exception.ExcessiveAttemptsException; +import com.usthe.sureness.processor.exception.ExpiredCredentialsException; +import com.usthe.sureness.processor.exception.IncorrectCredentialsException; +import com.usthe.sureness.processor.exception.NeedDigestInfoException; +import com.usthe.sureness.processor.exception.UnauthorizedException; +import com.usthe.sureness.processor.exception.UnknownAccountException; +import com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.SurenessContextHolder; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.io.IOException; +import java.io.PrintWriter; + +/** + * Override the default servlet filter + */ +public class SurenessJakartaServletFilter implements Filter { + + private final SecurityManager securityManager; + + public SurenessJakartaServletFilter(SecurityManager securityManager) { + this.securityManager = securityManager; + } + + /** logger **/ + private static final Logger logger = LoggerFactory.getLogger(SurenessJakartaServletFilter.class); + + private static final String UPGRADE = "Upgrade"; + + private static final String WEBSOCKET = "websocket"; + + @Override + public void init(FilterConfig filterConfig) { + logger.info("servlet surenessFilter initialized"); + } + + @Override + public void destroy() { + logger.info("servlet surenessFilter destroyed"); + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) + throws IOException, ServletException { + + try { + SubjectSum subject = securityManager.checkIn(servletRequest); + // You can consider using SurenessContextHolder to bind subject in threadLocal + // if bind, please remove it when end + if (subject != null) { + SurenessContextHolder.bindSubject(subject); + } + } catch (IncorrectCredentialsException | UnknownAccountException | ExpiredCredentialsException e1) { + logger.debug("this request account info is illegal, {}", e1.getMessage()); + responseWrite(ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body("Username or password is incorrect or token expired"), servletResponse); + return; + } catch (DisabledAccountException | ExcessiveAttemptsException e2) { + logger.debug("the account is disabled, {}", e2.getMessage()); + responseWrite(ResponseEntity + .status(HttpStatus.UNAUTHORIZED).body("Account is disabled"), servletResponse); + return; + } catch (NeedDigestInfoException e3) { + logger.debug("you should try once again with digest auth information"); + responseWrite(ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .header("WWW-Authenticate", e3.getAuthenticate()).build(), servletResponse); + return; + } catch (UnauthorizedException e4) { + logger.debug("this account can not access this resource, {}", e4.getMessage()); + responseWrite(ResponseEntity + .status(HttpStatus.FORBIDDEN) + .body("This account has no permission to access this resource"), servletResponse); + return; + } catch (RuntimeException e) { + logger.error("other exception happen: ", e); + responseWrite(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(), + servletResponse); + return; + } + + try { + // if ok, doFilter and add subject in request + filterChain.doFilter(servletRequest, servletResponse); + } finally { + int statusCode = ((HttpServletResponse) servletResponse).getStatus(); + String upgrade = ((HttpServletResponse) servletResponse).getHeader(UPGRADE); + if (statusCode != HttpStatus.SWITCHING_PROTOCOLS.value() || !WEBSOCKET.equals(upgrade)) { + SurenessContextHolder.clear(); + } + } + } + + /** + * write response json data + * @param content content + * @param response response + */ + private static void responseWrite(ResponseEntity content, ServletResponse response) { + response.setCharacterEncoding("UTF-8"); + ((HttpServletResponse) response).setStatus(content.getStatusCode().value()); + content.getHeaders().forEach((key, value) -> + ((HttpServletResponse) response).addHeader(key, value.get(0))); + try (PrintWriter printWriter = response.getWriter()) { + if (content.getBody() != null) { + printWriter.write(content.getBody().toString()); + } else { + printWriter.flush(); + } + } catch (IOException e) { + logger.error("responseWrite response error: ", e); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/sd/ServiceDiscoveryWorker.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/sd/ServiceDiscoveryWorker.java new file mode 100644 index 0000000..2ca8c48 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/sd/ServiceDiscoveryWorker.java @@ -0,0 +1,176 @@ +/* + * 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.manager.component.sd; + +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.arrow.RowWrapper; +import org.apache.hertzbeat.common.entity.manager.CollectorMonitorBind; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.MonitorBind; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException; +import org.apache.hertzbeat.common.util.BackoffUtils; +import org.apache.hertzbeat.common.util.ExponentialBackoff; +import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao; +import org.apache.hertzbeat.manager.dao.MonitorBindDao; +import org.apache.hertzbeat.manager.dao.MonitorDao; +import org.apache.hertzbeat.manager.dao.ParamDao; +import org.apache.hertzbeat.manager.scheduler.ManagerWorkerPool; +import org.apache.hertzbeat.manager.service.MonitorService; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Service Discovery Worker + */ +@Slf4j +@Component +public class ServiceDiscoveryWorker implements InitializingBean { + + private static final String FILED_HOST = "host"; + private static final String FILED_PORT = "port"; + private final MonitorService monitorService; + private final ParamDao paramDao; + private final MonitorDao monitorDao; + private final MonitorBindDao monitorBindDao; + private final CollectorMonitorBindDao collectorMonitorBindDao; + private final CommonDataQueue dataQueue; + private final ManagerWorkerPool workerPool; + + public ServiceDiscoveryWorker(MonitorService monitorService, ParamDao paramDao, MonitorDao monitorDao, + MonitorBindDao monitorBindDao, CollectorMonitorBindDao collectorMonitorBindDao, + CommonDataQueue dataQueue, ManagerWorkerPool workerPool) { + this.monitorService = monitorService; + this.paramDao = paramDao; + this.monitorDao = monitorDao; + this.monitorBindDao = monitorBindDao; + this.collectorMonitorBindDao = collectorMonitorBindDao; + this.dataQueue = dataQueue; + this.workerPool = workerPool; + } + + @Override + public void afterPropertiesSet() { + workerPool.executeLongRunning(new SdUpdateTask()); + } + + private class SdUpdateTask implements Runnable { + @Override + public void run() { + ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L); + while (!Thread.currentThread().isInterrupted()) { + try (final CollectRep.MetricsData metricsData = dataQueue.pollServiceDiscoveryData()) { + if (metricsData == null) { + continue; + } + backoff.reset(); + Long monitorId = metricsData.getId(); + final Monitor mainMonitor = monitorDao.findById(monitorId).orElse(null); + if (mainMonitor == null) { + log.warn("No monitor found for id {}", monitorId); + continue; + } + // collector + final Optional<CollectorMonitorBind> collectorBind = collectorMonitorBindDao.findCollectorMonitorBindByMonitorId(monitorId); + String collector = collectorBind.map(CollectorMonitorBind::getCollector).orElse(null); + // params + List<Param> mainMonitorParams = paramDao.findParamsByMonitorId(monitorId); + final Map<String, MonitorBind> subMonitorBindMap = monitorBindDao.findMonitorBindsByBizId(monitorId) + .stream().collect(Collectors.toMap(MonitorBind::getKeyStr, item -> item)); + RowWrapper rowWrapper = metricsData.readRow(); + Map<String, String> fieldsValue = Maps.newHashMapWithExpectedSize(8); + String defaultPort = mainMonitorParams.stream() + .filter(param -> FILED_PORT.equals(param.getField())) + .findFirst() + .map(Param::getParamValue) + .orElse(""); + while (rowWrapper.hasNextRow()) { + rowWrapper = rowWrapper.nextRow(); + fieldsValue.clear(); + rowWrapper.cellStream().forEach(cell -> { + String value = cell.getValue(); + fieldsValue.put(cell.getField().getName(), value); + }); + final String host = fieldsValue.get(FILED_HOST); + final String port = Optional.ofNullable(fieldsValue.get(FILED_PORT)) + .filter(p -> !p.isEmpty()) + .orElse(defaultPort); + final String keyStr = host + ":" + port; + final String instance = port.isEmpty() ? host : host + ":" + port; + if (subMonitorBindMap.containsKey(keyStr)) { + subMonitorBindMap.remove(keyStr); + continue; + } + Monitor newMonitor = mainMonitor.clone(); + newMonitor.setId(null); + newMonitor.setInstance(instance); + newMonitor.setName(newMonitor.getName() + "-" + host + ":" + port); + newMonitor.setScrape(CommonConstants.SCRAPE_STATIC); + newMonitor.setGmtCreate(LocalDateTime.now()); + newMonitor.setGmtUpdate(LocalDateTime.now()); + // replace host port + List<Param> newParams = new LinkedList<>(); + for (Param param : mainMonitorParams) { + Param newParam = param.clone(); + newParam.setId(null); + newParam.setGmtUpdate(null); + newParam.setGmtCreate(null); + if (FILED_HOST.equals(newParam.getField())) { + newParam.setParamValue(host); + } else if (FILED_PORT.equals(newParam.getField())) { + newParam.setParamValue(port); + } + newParams.add(newParam); + } + monitorService.addMonitor(newMonitor, newParams, collector, null); + MonitorBind monitorBind = MonitorBind.builder() + .bizId(monitorId) + .monitorId(newMonitor.getId()) + .keyStr(keyStr) + .build(); + monitorBindDao.save(monitorBind); + } + // hostMonitorMap only contains monitors which are already existed but not in service discovery data + // due to monitors that coincide with service discovery data are removed. + // Thus, all monitors still in hostMonitorMap need to be cancelled. + final Set<Long> needCancelMonitorIdSet = subMonitorBindMap.values().stream() + .map(MonitorBind::getMonitorId).collect(Collectors.toSet()); + monitorService.deleteMonitors(needCancelMonitorIdSet); + } catch (CommonDataQueueUnknownException ue) { + if (!BackoffUtils.shouldContinueAfterBackoff(backoff)) { + break; + } + } catch (Exception exception) { + log.error(exception.getMessage(), exception); + } + } + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/status/CalculateStatus.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/status/CalculateStatus.java new file mode 100644 index 0000000..8059094 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/status/CalculateStatus.java @@ -0,0 +1,388 @@ +/* + * 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.manager.component.status; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import jakarta.persistence.criteria.Predicate; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.StatusPageComponent; +import org.apache.hertzbeat.common.entity.manager.StatusPageHistory; +import org.apache.hertzbeat.common.entity.manager.StatusPageOrg; +import org.apache.hertzbeat.manager.config.StatusProperties; +import org.apache.hertzbeat.manager.dao.MonitorDao; +import org.apache.hertzbeat.manager.dao.StatusPageComponentDao; +import org.apache.hertzbeat.manager.dao.StatusPageHistoryDao; +import org.apache.hertzbeat.manager.dao.StatusPageOrgDao; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +/** + * calculate component status for status page + */ +@Component +@Slf4j +public class CalculateStatus implements DisposableBean { + + private static final int DEFAULT_CALCULATE_INTERVAL_TIME = 300; + + private final StatusPageOrgDao statusPageOrgDao; + + private final StatusPageComponentDao statusPageComponentDao; + + private final StatusPageHistoryDao statusPageHistoryDao; + + private final MonitorDao monitorDao; + + private final int intervals; + + private final ScheduledExecutorService calculateScheduler; + + private final ScheduledExecutorService combineHistoryScheduler; + + private final ExecutorService calculateExecutor; + + private final ExecutorService combineHistoryExecutor; + + private final ScheduledDispatchTask calculateTask; + + private final ScheduledDispatchTask combineHistoryTask; + + public CalculateStatus(StatusPageOrgDao statusPageOrgDao, StatusPageComponentDao statusPageComponentDao, + StatusProperties statusProperties, StatusPageHistoryDao statusPageHistoryDao, + MonitorDao monitorDao) { + this(statusPageOrgDao, statusPageComponentDao, statusProperties, statusPageHistoryDao, monitorDao, + VirtualThreadProperties.defaults(), true); + } + + @Autowired + public CalculateStatus(StatusPageOrgDao statusPageOrgDao, StatusPageComponentDao statusPageComponentDao, + StatusProperties statusProperties, StatusPageHistoryDao statusPageHistoryDao, + MonitorDao monitorDao, VirtualThreadProperties virtualThreadProperties) { + this(statusPageOrgDao, statusPageComponentDao, statusProperties, statusPageHistoryDao, monitorDao, + virtualThreadProperties, true); + } + + CalculateStatus(StatusPageOrgDao statusPageOrgDao, StatusPageComponentDao statusPageComponentDao, + StatusProperties statusProperties, StatusPageHistoryDao statusPageHistoryDao, + MonitorDao monitorDao, VirtualThreadProperties virtualThreadProperties, boolean autoStart) { + this.statusPageOrgDao = statusPageOrgDao; + this.monitorDao = monitorDao; + this.statusPageComponentDao = statusPageComponentDao; + this.statusPageHistoryDao = statusPageHistoryDao; + intervals = statusProperties.getCalculate() == null ? DEFAULT_CALCULATE_INTERVAL_TIME : statusProperties.getCalculate().getInterval(); + this.calculateScheduler = createScheduler("status-page-calculate-%d", "Status calculate has uncaughtException."); + this.combineHistoryScheduler = createScheduler("status-page-history-%d", "History combine has uncaughtException."); + this.calculateExecutor = createVirtualExecutor(virtualThreadProperties, "status-page-calculate-vt-", + "Status calculate worker has uncaughtException."); + this.combineHistoryExecutor = createVirtualExecutor(virtualThreadProperties, "status-page-history-vt-", + "History combine worker has uncaughtException."); + this.calculateTask = new ScheduledDispatchTask(calculateExecutor, this::runCalculate); + this.combineHistoryTask = new ScheduledDispatchTask(combineHistoryExecutor, this::runCombineHistory); + if (autoStart) { + startCalculate(); + startCombineHistory(); + } + } + + private void startCalculate() { + calculateScheduler.scheduleAtFixedRate(this::dispatchCalculate, 5, intervals, TimeUnit.SECONDS); + } + + private void startCombineHistory() { + // combine history every day at 1:00 AM + LocalDateTime now = LocalDateTime.now(); + LocalDateTime nextRun = now.withHour(1).withMinute(0).withSecond(0); + if (now.isAfter(nextRun)) { + nextRun = nextRun.plusDays(1); + } + long delay = Duration.between(now, nextRun).toMillis(); + combineHistoryScheduler.scheduleAtFixedRate(this::dispatchCombineHistory, delay, + TimeUnit.DAYS.toMillis(1), TimeUnit.MILLISECONDS); + } + + /** + * get calculate status intervals + * @return intervals + */ + public int getCalculateStatusIntervals() { + return intervals; + } + + void dispatchCalculate() { + calculateTask.dispatch(); + } + + void dispatchCombineHistory() { + combineHistoryTask.dispatch(); + } + + @Override + public void destroy() { + calculateScheduler.shutdownNow(); + combineHistoryScheduler.shutdownNow(); + if (calculateExecutor != null) { + calculateExecutor.shutdownNow(); + } + if (combineHistoryExecutor != null) { + combineHistoryExecutor.shutdownNow(); + } + } + + private void runCalculate() { + log.info("start to calculate status page state"); + try { + // calculate component state from tag bind monitors status + List<StatusPageOrg> statusPageOrgList = statusPageOrgDao.findAll(); + for (StatusPageOrg statusPageOrg : statusPageOrgList) { + long orgId = statusPageOrg.getId(); + List<StatusPageComponent> pageComponentList = statusPageComponentDao.findByOrgId(orgId); + Set<Byte> stateSet = new HashSet<>(8); + for (StatusPageComponent component : pageComponentList) { + byte state; + if (component.getMethod() == CommonConstants.STATUS_PAGE_CALCULATE_METHOD_MANUAL) { + state = component.getConfigState(); + } else { + Map<String, String> labels = component.getLabels(); + if (labels == null || labels.isEmpty()) { + continue; + } + Specification<Monitor> specification = (root, query, criteriaBuilder) -> { + List<Predicate> predicates = new ArrayList<>(); + // create every label condition + labels.forEach((key, value) -> { + String pattern = String.format("%%\"%s\":\"%s\"%%", key, value); + predicates.add(criteriaBuilder.like(root.get("labels"), pattern)); + }); + + // use or connect them + return criteriaBuilder.or(predicates.toArray(new Predicate[0])); + }; + List<Monitor> monitorList = monitorDao.findAll(specification); + state = CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN; + for (Monitor monitor : monitorList) { + if (monitor.getStatus() == CommonConstants.MONITOR_DOWN_CODE) { + state = CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL; + break; + } else if (monitor.getStatus() == CommonConstants.MONITOR_UP_CODE) { + state = CommonConstants.STATUS_PAGE_COMPONENT_STATE_NORMAL; + } + } + } + stateSet.add(state); + component.setState(state); + statusPageComponentDao.save(component); + // insert component state history + StatusPageHistory statusPageHistory = StatusPageHistory.builder() + .componentId(component.getId()) + .state(state) + .timestamp(System.currentTimeMillis()) + .build(); + statusPageHistoryDao.save(statusPageHistory); + } + stateSet.remove(CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN); + if (stateSet.remove(CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL)) { + if (stateSet.contains(CommonConstants.STATUS_PAGE_COMPONENT_STATE_NORMAL)) { + statusPageOrg.setState(CommonConstants.STATUS_PAGE_ORG_STATE_SOME_ABNORMAL); + } else { + statusPageOrg.setState(CommonConstants.STATUS_PAGE_ORG_STATE_ALL_ABNORMAL); + } + } else { + statusPageOrg.setState(CommonConstants.STATUS_PAGE_ORG_STATE_ALL_NORMAL); + } + statusPageOrg.setGmtUpdate(LocalDateTime.now()); + statusPageOrgDao.save(statusPageOrg); + } + } catch (Exception e) { + log.error("status page calculate component state error: {}", e.getMessage(), e); + } + } + + private void runCombineHistory() { + try { + // combine pre day status history to one record + LocalDateTime nowTime = LocalDateTime.now(); + ZoneOffset zoneOffset = ZoneId.systemDefault().getRules().getOffset(Instant.now()); + LocalDateTime midnight = nowTime.withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime preNight = midnight.minusDays(1); + long midnightTimestamp = midnight.toInstant(zoneOffset).toEpochMilli(); + long preNightTimestamp = preNight.toInstant(zoneOffset).toEpochMilli(); + List<StatusPageHistory> statusPageHistoryList = statusPageHistoryDao + .findStatusPageHistoriesByTimestampBetween(preNightTimestamp, midnightTimestamp); + Map<Long, StatusPageHistory> statusPageHistoryMap = new HashMap<>(8); + for (StatusPageHistory statusPageHistory : statusPageHistoryList) { + statusPageHistory.setNormal(0); + statusPageHistory.setAbnormal(0); + statusPageHistory.setUnknowing(0); + if (statusPageHistoryMap.containsKey(statusPageHistory.getComponentId())) { + StatusPageHistory history = statusPageHistoryMap.get(statusPageHistory.getComponentId()); + if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL) { + history.setAbnormal(history.getAbnormal() + intervals); + } else if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN) { + history.setUnknowing(history.getUnknowing() + intervals); + } else { + history.setNormal(history.getNormal() + intervals); + } + statusPageHistoryMap.put(statusPageHistory.getComponentId(), history); + } else { + if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL) { + statusPageHistory.setAbnormal(intervals); + } else if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN) { + statusPageHistory.setUnknowing(intervals); + } else { + statusPageHistory.setNormal(intervals); + } + statusPageHistoryMap.put(statusPageHistory.getComponentId(), statusPageHistory); + } + } + statusPageHistoryDao.deleteAll(statusPageHistoryList); + for (StatusPageHistory history : statusPageHistoryMap.values()) { + double total = history.getNormal() + history.getAbnormal() + history.getUnknowing(); + double uptime = 0; + if (total > 0) { + uptime = (double) history.getNormal() / total; + } + history.setUptime(uptime); + if (history.getAbnormal() > 0) { + history.setState(CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL); + } else if (history.getNormal() > 0) { + history.setState(CommonConstants.STATUS_PAGE_COMPONENT_STATE_NORMAL); + } else { + history.setState(CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN); + } + statusPageHistoryDao.save(history); + } + } catch (Exception e) { + log.error("status page combine history error: {}", e.getMessage(), e); + } + } + + private ScheduledExecutorService createScheduler(String threadNameFormat, String errorMessage) { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error(errorMessage); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat(threadNameFormat) + .build(); + return Executors.newSingleThreadScheduledExecutor(threadFactory); + } + + private ExecutorService createVirtualExecutor(VirtualThreadProperties virtualThreadProperties, String threadPrefix, + String errorMessage) { + VirtualThreadProperties properties = + virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties; + if (!properties.enabled()) { + return null; + } + return Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name(threadPrefix, 0) + .uncaughtExceptionHandler((thread, throwable) -> { + log.error(errorMessage); + log.error(throwable.getMessage(), throwable); + }) + .factory()); + } + + private static final class ScheduledDispatchTask { + + private final ExecutorService executorService; + private final Runnable task; + private final Object lock = new Object(); + private boolean running; + private int pendingRuns; + + private ScheduledDispatchTask(ExecutorService executorService, Runnable task) { + this.executorService = executorService; + this.task = task; + } + + private void dispatch() { + if (executorService == null) { + task.run(); + return; + } + synchronized (lock) { + if (running) { + pendingRuns++; + return; + } + running = true; + } + submit(); + } + + private void submit() { + boolean submitted = false; + try { + executorService.execute(() -> { + try { + task.run(); + } finally { + onComplete(); + } + }); + submitted = true; + } finally { + if (!submitted) { + synchronized (lock) { + running = false; + pendingRuns = 0; + } + } + } + } + + private void onComplete() { + boolean shouldRunAgain; + synchronized (lock) { + if (pendingRuns > 0) { + pendingRuns--; + shouldRunAgain = true; + } else { + running = false; + shouldRunAgain = false; + } + } + if (shouldRunAgain) { + submit(); + } + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/ParamValidator.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/ParamValidator.java new file mode 100644 index 0000000..e51b593 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/ParamValidator.java @@ -0,0 +1,44 @@ +/* + * 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.manager.component.validator; + +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; + +/** + * Parameter validator interface + */ +public interface ParamValidator { + + /** + * Check if the validator supports the given parameter type + * + * @param type parameter type + * @return true if supported + */ + boolean support(String type); + + /** + * Validate the parameter + * + * @param paramDefine parameter definition + * @param param parameter actual value + * @throws IllegalArgumentException if validation fails + */ + void validate(ParamDefineInfo paramDefine, MonitorParam param) throws IllegalArgumentException; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/ParamValidatorManager.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/ParamValidatorManager.java new file mode 100644 index 0000000..b7d1b65 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/ParamValidatorManager.java @@ -0,0 +1,48 @@ +/* + * 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.manager.component.validator; + +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * Parameter validator manager + */ +@Component +public class ParamValidatorManager { + + private final List<ParamValidator> validators; + + public ParamValidatorManager(List<ParamValidator> validators) { + this.validators = validators; + } + + public void validate(ParamDefineInfo paramDefine, MonitorParam param) { + for (ParamValidator validator : validators) { + if (validator.support(paramDefine.getType())) { + validator.validate(paramDefine, param); + return; + } + } + // No validator found for the given type. + throw new IllegalArgumentException("ParamDefine type " + paramDefine.getType() + " is invalid."); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/ArrayParamValidator.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/ArrayParamValidator.java new file mode 100644 index 0000000..e1ef0ff --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/ArrayParamValidator.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.component.validator.impl; + +import org.apache.hertzbeat.manager.component.validator.ParamValidator; +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.springframework.stereotype.Component; + +/** + * Array parameter validator + */ +@Component +public class ArrayParamValidator implements ParamValidator { + @Override + public boolean support(String type) { + return "array".equals(type); + } + + @Override + public void validate(ParamDefineInfo paramDefine, MonitorParam param) { + String[] arrays = param.getParamValue().split(","); + if (arrays.length == 0) { + throw new IllegalArgumentException("Param field " + paramDefine.getField() + " value " + + param.getParamValue() + " is invalid arrays value"); + } + if (param.getParamValue().startsWith("[") && param.getParamValue().endsWith("]")) { + param.setParamValue(param.getParamValue().substring(1, param.getParamValue().length() - 1)); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/BooleanParamValidator.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/BooleanParamValidator.java new file mode 100644 index 0000000..30a9110 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/BooleanParamValidator.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.component.validator.impl; + +import org.apache.hertzbeat.manager.component.validator.ParamValidator; +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.springframework.stereotype.Component; + +/** + * Boolean parameter validator + */ +@Component +public class BooleanParamValidator implements ParamValidator { + @Override + public boolean support(String type) { + return "boolean".equals(type); + } + + @Override + public void validate(ParamDefineInfo paramDefine, MonitorParam param) { + String booleanValue = param.getParamValue(); + if (!"true".equalsIgnoreCase(booleanValue) && !"false".equalsIgnoreCase(booleanValue)) { + throw new IllegalArgumentException("Params field " + paramDefine.getField() + " value " + + booleanValue + " is invalid boolean value."); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/HostParamValidatorAdapter.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/HostParamValidatorAdapter.java new file mode 100644 index 0000000..f5fe012 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/HostParamValidatorAdapter.java @@ -0,0 +1,49 @@ +/* + * 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.manager.component.validator.impl; + +import org.apache.hertzbeat.manager.component.validator.ParamValidator; +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.springframework.stereotype.Component; + +/** + * Host parameter validator that delegates to the common HostParamValidator + */ +@Component +public class HostParamValidatorAdapter implements ParamValidator { + + private final org.apache.hertzbeat.common.support.valid.HostParamValidator hostValidator; + + public HostParamValidatorAdapter() { + this.hostValidator = new org.apache.hertzbeat.common.support.valid.HostParamValidator(); + } + + @Override + public boolean support(String type) { + return "host".equals(type); + } + + @Override + public void validate(ParamDefineInfo paramDefine, MonitorParam param) { + if (!hostValidator.isValid(param.getParamValue(), null)) { + throw new IllegalArgumentException("Params field " + paramDefine.getField() + " value " + + param.getParamValue() + " is invalid host value."); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/JsonParamValidator.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/JsonParamValidator.java new file mode 100644 index 0000000..7214bf5 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/JsonParamValidator.java @@ -0,0 +1,45 @@ +/* + * 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.manager.component.validator.impl; + +import tools.jackson.core.type.TypeReference; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.component.validator.ParamValidator; +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.springframework.stereotype.Component; + +/** + * JSON parameter validator + */ +@Component +public class JsonParamValidator implements ParamValidator { + @Override + public boolean support(String type) { + return "metrics-field".equals(type) || "key-value".equals(type); + } + + @Override + public void validate(ParamDefineInfo paramDefine, MonitorParam param) { + if (JsonUtil.fromJson(param.getParamValue(), new TypeReference<>() { + }) == null) { + throw new IllegalArgumentException("Params field " + paramDefine.getField() + " value " + + param.getParamValue() + " is invalid key-value value"); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/NumberParamValidator.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/NumberParamValidator.java new file mode 100644 index 0000000..3a313a2 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/NumberParamValidator.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.component.validator.impl; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.util.IntervalExpressionUtil; +import org.apache.hertzbeat.manager.component.validator.ParamValidator; +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.springframework.stereotype.Component; + +/** + * Number parameter validator + */ +@Component +public class NumberParamValidator implements ParamValidator { + @Override + public boolean support(String type) { + return "number".equals(type); + } + + @Override + public void validate(ParamDefineInfo paramDefine, MonitorParam param) { + Double doubleValue = org.apache.hertzbeat.common.util.CommonUtil.parseStrDouble(param.getParamValue()); + if (doubleValue == null) { + throw new IllegalArgumentException("Params field " + paramDefine.getField() + " type " + + paramDefine.getType() + " is invalid."); + } + if (paramDefine.getRange() != null) { + if (!IntervalExpressionUtil.validNumberIntervalExpress(doubleValue, + paramDefine.getRange())) { + throw new IllegalArgumentException("Params field " + paramDefine.getField() + " type " + + paramDefine.getType() + " over range " + paramDefine.getRange()); + } + } + param.setType(CommonConstants.PARAM_TYPE_NUMBER); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/OptionParamValidator.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/OptionParamValidator.java new file mode 100644 index 0000000..7e132f3 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/OptionParamValidator.java @@ -0,0 +1,54 @@ +/* + * 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.manager.component.validator.impl; + +import org.apache.hertzbeat.manager.component.validator.ParamValidator; +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * Option parameter validator for radio and checkbox types + */ +@Component +public class OptionParamValidator implements ParamValidator { + @Override + public boolean support(String type) { + return "radio".equals(type) || "checkbox".equals(type); + } + + @Override + public void validate(ParamDefineInfo paramDefine, MonitorParam param) { + List<ParamDefineInfo.OptionInfo> options = paramDefine.getOptions(); + boolean invalid = true; + if (options != null) { + for (ParamDefineInfo.OptionInfo option : options) { + if (param.getParamValue().equalsIgnoreCase(option.getValue())) { + invalid = false; + break; + } + } + } + if (invalid) { + throw new IllegalArgumentException("Params field " + paramDefine.getField() + " value " + + param.getParamValue() + " is invalid option value"); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidator.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidator.java new file mode 100644 index 0000000..7183d47 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidator.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.component.validator.impl; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.util.AesUtil; +import org.apache.hertzbeat.manager.component.validator.ParamValidator; +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.springframework.stereotype.Component; + +/** + * Password parameter validator + */ +@Component +public class PasswordParamValidator implements ParamValidator { + @Override + public boolean support(String type) { + return "password".equals(type); + } + + @Override + public void validate(ParamDefineInfo paramDefine, MonitorParam param) { + String passwordValue = param.getParamValue(); + if (!AesUtil.isCiphertext(passwordValue)) { + passwordValue = AesUtil.aesEncode(passwordValue); + param.setParamValue(passwordValue); + } + param.setType(CommonConstants.PARAM_TYPE_PASSWORD); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/TextParamValidator.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/TextParamValidator.java new file mode 100644 index 0000000..67e10bf --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/TextParamValidator.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.component.validator.impl; + +import org.apache.hertzbeat.manager.component.validator.ParamValidator; +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.springframework.stereotype.Component; + +/** + * Text parameter validator + */ +@Component +public class TextParamValidator implements ParamValidator { + @Override + public boolean support(String type) { + return "text".equals(type) || "textarea".equals(type); + } + + @Override + public void validate(ParamDefineInfo paramDefine, MonitorParam param) { + Short limit = paramDefine.getLimit(); + if (limit != null && param.getParamValue().length() > limit) { + throw new IllegalArgumentException("Params field " + paramDefine.getField() + " type " + + paramDefine.getType() + " over limit " + limit); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/AngularErrorViewResolver.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/AngularErrorViewResolver.java new file mode 100644 index 0000000..414265d --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/AngularErrorViewResolver.java @@ -0,0 +1,145 @@ +/* + * 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.manager.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; +import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.webmvc.autoconfigure.error.ErrorViewResolver; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.View; + +/** + * Solve the front-end routing problem of angular static website resources with DefaultErrorViewResolver and route the 404 website request to the angular front-end + */ +@Configuration +@Slf4j +public class AngularErrorViewResolver implements ErrorViewResolver, Ordered { + + private static final Map<HttpStatus.Series, String> SERIES_VIEWS; + private static final String NOT_FOUND_CODE = "404"; + + static { + Map<HttpStatus.Series, String> views = new EnumMap<>(HttpStatus.Series.class); + views.put(HttpStatus.Series.CLIENT_ERROR, "4xx"); + views.put(HttpStatus.Series.SERVER_ERROR, "5xx"); + SERIES_VIEWS = Collections.unmodifiableMap(views); + } + + private final ApplicationContext applicationContext; + + private final WebProperties.Resources resources; + + private final TemplateAvailabilityProviders templateAvailabilityProviders; + + private int order = Ordered.LOWEST_PRECEDENCE; + + public AngularErrorViewResolver(ApplicationContext applicationContext, WebProperties webProperties) { + Assert.notNull(applicationContext, "ApplicationContext must not be null"); + Assert.notNull(webProperties.getResources(), "Resources must not be null"); + this.applicationContext = applicationContext; + this.resources = webProperties.getResources(); + this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext); + } + + @Override + public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { + ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); + if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { + modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); + } + return modelAndView; + } + + private ModelAndView resolve(String viewName, Map<String, Object> model) { + String errorViewName = "error/" + viewName; + if (NOT_FOUND_CODE.equals(viewName)) { + errorViewName = "index"; + } + TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, + this.applicationContext); + if (provider != null) { + return new ModelAndView(errorViewName, model); + } + return resolveResource(errorViewName, model); + } + + private ModelAndView resolveResource(String viewName, Map<String, Object> model) { + for (String location : this.resources.getStaticLocations()) { + try { + Resource resource = this.applicationContext.getResource(location); + resource = resource.createRelative(viewName + ".html"); + if (resource.exists()) { + return new ModelAndView(new HtmlResourceView(resource), model); + } + } catch (Exception ex) { + log.error("Error resolving resource", ex); + } + } + return null; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + /** + * {@link View} backed by an HTML resource. + */ + private static class HtmlResourceView implements View { + + private final Resource resource; + + HtmlResourceView(Resource resource) { + this.resource = resource; + } + + @Override + public String getContentType() { + return MediaType.TEXT_HTML_VALUE; + } + + @Override + public void render(Map<String, ?> model, @NonNull HttpServletRequest request, HttpServletResponse response) + throws Exception { + response.setContentType(getContentType()); + FileCopyUtils.copy(this.resource.getInputStream(), response.getOutputStream()); + } + + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationConfiguration.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationConfiguration.java new file mode 100644 index 0000000..ebd114d --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationConfiguration.java @@ -0,0 +1,40 @@ +/* + * 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.manager.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Registers the managed API token validation interceptor after servlet filters. + */ +@Configuration +public class ApiTokenValidationConfiguration implements WebMvcConfigurer { + + private final ApiTokenValidationFilter apiTokenValidationFilter; + + public ApiTokenValidationConfiguration(ApiTokenValidationFilter apiTokenValidationFilter) { + this.apiTokenValidationFilter = apiTokenValidationFilter; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(apiTokenValidationFilter).addPathPatterns("/api/**"); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java new file mode 100644 index 0000000..4075d2e --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.config; + +import com.usthe.sureness.subject.PrincipalMap; +import com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.SurenessContextHolder; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.service.AccountService; +import org.apache.hertzbeat.manager.service.impl.AccountServiceImpl; +import org.jspecify.annotations.NonNull; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * Interceptor to validate that managed API tokens have not been revoked (deleted). + * <p> + * This interceptor runs AFTER the Sureness authentication filter to ensure: + * 1. Only authenticated requests are checked (avoids unnecessary DB hits) + * 2. lastUsedTime is only updated for successfully authenticated requests + * <p> + * Responsibility split: + * - <b>Expiration</b>: handled by Sureness/JWT natively via the {@code exp} claim + * - <b>Revocation</b>: handled here by checking the token's status in the database + * - <b>Last used time</b>: updated here on each valid request (throttled) + * <p> + * Only tokens containing the "managed" claim (generated by the new token management system) + * are validated against the database. Legacy tokens without this claim are allowed to pass + * through for backward compatibility. + * </p> + */ +@Component +@Slf4j +public class ApiTokenValidationFilter implements HandlerInterceptor { + + private static final String ROLES_CLAIM = "roles"; + private static final String TOKEN_VALIDATION_UNAVAILABLE = "Token validation unavailable"; + + private final AccountService accountService; + + public ApiTokenValidationFilter(AccountService accountService) { + this.accountService = accountService; + } + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) + throws IOException { + SubjectSum subject = SurenessContextHolder.getBindSubject(); + if (subject == null || !isManagedToken(subject)) { + return true; + } + String authorization = request.getHeader(NetworkConstants.AUTHORIZATION); + + if (authorization != null && authorization.startsWith(DispatchConstants.BEARER)) { + String token = authorization.substring(DispatchConstants.BEARER.length()).trim(); + if (!token.isEmpty()) { + try { + String rejectReason = checkManagedToken(subject, token); + if (rejectReason != null) { + return writeError(response, HttpStatus.UNAUTHORIZED, rejectReason); + } + } catch (RuntimeException e) { + log.warn("Managed token validation failed", e); + return writeError(response, HttpStatus.SERVICE_UNAVAILABLE, TOKEN_VALIDATION_UNAVAILABLE); + } + } + } + + return true; + } + + /** + * Check if a managed token should be rejected. + * <p> + * Returns a rejection reason string if the token should be rejected, null if allowed. + * Checks: + * 1. Only managed tokens (with "managed" claim) are validated — legacy tokens pass through + * 2. Token must exist and be active in the database (not deleted/revoked) + * 3. Token owner must still exist, remain active, and still own the claimed roles + * If the token is valid, updates the last used time. + * </p> + */ + private String checkManagedToken(SubjectSum subject, String token) { + String rejectReason = accountService.checkTokenStatus(token); + if (rejectReason != null) { + return rejectReason; + } + rejectReason = accountService.checkManagedTokenAccess(getCurrentUserId(subject), extractClaimedRoles(subject)); + if (rejectReason != null) { + return rejectReason; + } + try { + accountService.touchTokenLastUsedTime(token); + } catch (RuntimeException e) { + log.debug("Failed to update token last used time", e); + } + return null; + } + + private boolean isManagedToken(SubjectSum subject) { + PrincipalMap principalMap = subject.getPrincipalMap(); + if (principalMap == null) { + return false; + } + Object managed = principalMap.getPrincipal(AccountServiceImpl.CLAIM_MANAGED); + return managed instanceof Boolean ? (Boolean) managed : Boolean.parseBoolean(String.valueOf(managed)); + } + + @SuppressWarnings("unchecked") + private List<String> extractClaimedRoles(SubjectSum subject) { + Object roles = subject.getRoles(); + if (roles instanceof List<?>) { + return (List<String>) roles; + } + PrincipalMap principalMap = subject.getPrincipalMap(); + if (principalMap == null) { + return Collections.emptyList(); + } + Object claimedRoles = principalMap.getPrincipal(ROLES_CLAIM); + if (claimedRoles instanceof List<?>) { + return (List<String>) claimedRoles; + } + return Collections.emptyList(); + } + + private String getCurrentUserId(SubjectSum subject) { + Object principal = subject.getPrincipal(); + return principal == null ? null : String.valueOf(principal); + } + + private boolean writeError(HttpServletResponse response, HttpStatus status, String message) throws IOException { + response.setStatus(status.value()); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json"); + try (PrintWriter writer = response.getWriter()) { + writer.write(JsonUtil.toJson(Map.of("code", status.value(), "msg", message))); + } + return false; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ConfigInitializer.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ConfigInitializer.java new file mode 100644 index 0000000..fca66f8 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ConfigInitializer.java @@ -0,0 +1,189 @@ +/* + * 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.manager.config; + +import com.usthe.sureness.util.JsonWebTokenUtil; +import jakarta.annotation.Resource; +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.TimeZone; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.apache.hertzbeat.common.util.AesUtil; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.common.util.TimeZoneUtil; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.manager.pojo.dto.MuteConfig; +import org.apache.hertzbeat.manager.pojo.dto.SystemConfig; +import org.apache.hertzbeat.manager.pojo.dto.SystemSecret; +import org.apache.hertzbeat.manager.pojo.dto.TemplateConfig; +import org.apache.hertzbeat.manager.service.AppService; +import org.apache.hertzbeat.manager.service.impl.MuteGeneralConfigServiceImpl; +import org.apache.hertzbeat.manager.service.impl.SystemGeneralConfigServiceImpl; +import org.apache.hertzbeat.manager.service.impl.SystemSecretServiceImpl; +import org.apache.hertzbeat.manager.service.impl.TemplateConfigServiceImpl; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.SmartLifecycle; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * Config Initializer + */ +@Component +@Order(value = Ordered.HIGHEST_PRECEDENCE + 2) +public class ConfigInitializer implements SmartLifecycle { + + private boolean running = false; + + private static final String DEFAULT_JWT_SECRET = "CyaFv0bwq2Eik0jdrKUtsA6bx3sDJeFV643R " + + "LnfKefTjsIfJLBa2YkhEqEGtcHDTNe4CU6+9 " + + "8tVt4bisXQ13rbN0oxhUZR73M6EByXIO+SV5 " + + "dKhaX0csgOCTlCxq20yhmUea6H6JIpSE2Rwp"; + + @Value("${sureness.jwt.secret:" + DEFAULT_JWT_SECRET + "}") + private String currentJwtSecret; + + @Value("${common.secret:" + AesUtil.DEFAULT_ENCODE_RULES + "}") + private String currentAesSecret; + + @Resource + private SystemGeneralConfigServiceImpl systemGeneralConfigService; + + @Resource + private SystemSecretServiceImpl systemSecretService; + + @Resource + private TemplateConfigServiceImpl templateConfigService; + + @Resource + private MuteGeneralConfigServiceImpl muteGeneralConfigService; + + @Resource + private AppService appService; + + @Resource + protected GeneralConfigDao generalConfigDao; + + @SneakyThrows + public void initConfig() { + // for system config + SystemConfig systemConfig = systemGeneralConfigService.getConfig(); + if (systemConfig != null) { + TimeZoneUtil.setTimeZoneAndLocale(systemConfig.getTimeZoneId(), systemConfig.getLocale()); + + final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX"); + simpleDateFormat.setTimeZone(TimeZone.getDefault()); + } else { + // init system config data + systemConfig = SystemConfig.builder().timeZoneId(TimeZone.getDefault().getID()).theme("default") + .locale(Locale.getDefault().getLanguage() + CommonConstants.LOCALE_SEPARATOR + + Locale.getDefault().getCountry()) + .build(); + String contentJson = JsonUtil.toJson(systemConfig); + GeneralConfig generalConfig2Save = GeneralConfig.builder() + .type(systemGeneralConfigService.type()) + .content(contentJson) + .build(); + generalConfigDao.save(generalConfig2Save); + } + // for template config, flush the template config in db to memory + TemplateConfig templateConfig = templateConfigService.getConfig(); + appService.updateCustomTemplateConfig(templateConfig); + // for system jwt secrets and aes secret + boolean needUpdate = false; + SystemSecret.SystemSecretBuilder systemSecretBuilder = SystemSecret.builder(); + SystemSecret systemSecret = systemSecretService.getConfig(); + if (systemSecret != null) { + systemSecretBuilder.jwtSecret(systemSecret.getJwtSecret()); + systemSecretBuilder.aesSecret(systemSecret.getAesSecret()); + } + if (DEFAULT_JWT_SECRET.equals(currentJwtSecret)) { + // use the random jwt secret + if (systemSecret == null || StringUtils.isBlank(systemSecret.getJwtSecret())) { + currentJwtSecret = randomizeSecret(DEFAULT_JWT_SECRET); + systemSecretBuilder.jwtSecret(currentJwtSecret); + needUpdate = true; + } else { + currentJwtSecret = systemSecret.getJwtSecret(); + } + } + // else use the user custom jwt secret, set the jwt secret token in util + JsonWebTokenUtil.setDefaultSecretKey(currentJwtSecret); + // Aes secret config + if (AesUtil.DEFAULT_ENCODE_RULES.equals(currentAesSecret)) { + // use the random aes secret + if (systemSecret == null || StringUtils.isBlank(systemSecret.getAesSecret())) { + currentAesSecret = randomizeSecret(AesUtil.DEFAULT_ENCODE_RULES); + systemSecretBuilder.aesSecret(currentAesSecret); + needUpdate = true; + } else { + currentAesSecret = systemSecret.getAesSecret(); + } + } + AesUtil.setDefaultSecretKey(currentAesSecret); + if (needUpdate) { + systemSecretService.saveConfig(systemSecretBuilder.build()); + } + // init web-app mute config + MuteConfig muteConfig = muteGeneralConfigService.getConfig(); + if (muteConfig == null) { + muteConfig = MuteConfig.builder().mute(true).build(); + muteGeneralConfigService.saveConfig(muteConfig); + } + } + + @Override + public void start() { + initConfig(); + running = true; + } + + @Override + public void stop() { + running = false; + } + + @Override + public boolean isRunning() { + return running; + } + + @Override + public int getPhase() { + return Ordered.HIGHEST_PRECEDENCE; + } + + private String randomizeSecret(String secret) { + SecureRandom random = new SecureRandom(); + StringBuilder sb = new StringBuilder(secret.length()); + for (int i = 0; i < secret.length(); i++) { + char ch; + do { + int codePoint = random.nextInt('z' - '0' + 1) + '0'; + ch = (char) codePoint; + } while (!Character.isLetterOrDigit(ch)); + sb.append(ch); + } + return sb.toString(); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/FlywayConfiguration.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/FlywayConfiguration.java new file mode 100644 index 0000000..3947674 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/FlywayConfiguration.java @@ -0,0 +1,62 @@ +/* + * 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.manager.config; + +import org.flywaydb.core.Flyway; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.flyway.autoconfigure.FlywayMigrationInitializer; +import org.springframework.boot.flyway.autoconfigure.FlywayProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; + +/** + * Flyway database migration config. + * Delays Flyway execution until after Hibernate has created/updated the schema. + */ +@Configuration +@ConditionalOnProperty(prefix = "spring.flyway", name = "enabled", havingValue = "true") +public class FlywayConfiguration { + + /** + * Disable the default FlywayMigrationInitializer by providing an empty callback. + */ + @Bean + public FlywayMigrationInitializer flywayInitializer(Flyway flyway) { + return new FlywayMigrationInitializer(flyway, (f) -> { + // Empty callback - we'll run migrations manually after Hibernate + }); + } + + /** + * Delayed Flyway migration that runs after EntityManagerFactory is initialized. + * This ensures Hibernate's ddl-auto runs first to create/update tables, + * then Flyway can perform additional migrations if needed. + */ + @Bean + @DependsOn("entityManagerFactory") + Dummy delayedFlywayInitializer(Flyway flyway, FlywayProperties flywayProperties) { + if (flywayProperties.isEnabled()) { + flyway.migrate(); + } + return new Dummy(); + } + + static class Dummy { + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/HeaderRequestInterceptor.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/HeaderRequestInterceptor.java new file mode 100644 index 0000000..08a9252 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/HeaderRequestInterceptor.java @@ -0,0 +1,45 @@ +/* + * 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.manager.config; + +import java.io.IOException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.lang.NonNull; + +/** + * Rest Template interceptor adds request header information + */ +public class HeaderRequestInterceptor implements ClientHttpRequestInterceptor { + + @Override + public ClientHttpResponse intercept(HttpRequest request, @NonNull byte[] body, @NonNull ClientHttpRequestExecution execution) + throws IOException { + // Send json by default + if (request.getHeaders().getContentType() == null) { + request.getHeaders().setContentType(MediaType.APPLICATION_JSON); + } + // Use short links + request.getHeaders().add(HttpHeaders.CONNECTION, "close"); + return execution.execute(request, body); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/JacksonConfig.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/JacksonConfig.java new file mode 100644 index 0000000..efd032d --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/JacksonConfig.java @@ -0,0 +1,48 @@ +/* + * 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.manager.config; + +import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tools.jackson.databind.DeserializationFeature; + +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +/** + * Jackson config. + */ +@Configuration +public class JacksonConfig { + + @Bean + public JsonMapperBuilderCustomizer jsonMapperBuilderCustomizer() { + return builder -> { + final String dateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSX"; + final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateTimeFormat); + simpleDateFormat.setTimeZone(TimeZone.getDefault()); + + builder.defaultTimeZone(TimeZone.getDefault()); + builder.defaultDateFormat(simpleDateFormat); + + builder.disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES); + builder.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + }; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/JpaAuditorConfig.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/JpaAuditorConfig.java new file mode 100644 index 0000000..c8b7df5 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/JpaAuditorConfig.java @@ -0,0 +1,44 @@ +/* + * 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.manager.config; + +import com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.SurenessContextHolder; +import java.util.Optional; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; + +/** + * generate auditor for jpa auditing + */ +@Configuration +public class JpaAuditorConfig implements AuditorAware<String> { + + @Override + public Optional<String> getCurrentAuditor() { + SubjectSum subjectSum = SurenessContextHolder.getBindSubject(); + String username = null; + if (subjectSum != null) { + Object principal = subjectSum.getPrincipal(); + if (principal != null) { + username = String.valueOf(principal); + } + } + return Optional.ofNullable(username); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ManagerAutoConfiguration.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ManagerAutoConfiguration.java new file mode 100644 index 0000000..4e0c342 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ManagerAutoConfiguration.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.config; + +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.ComponentScan; + +/** + * Manager auto configuration. + */ + +@AutoConfiguration +@ComponentScan(basePackages = ConfigConstants.PkgConstant.PKG + + SignConstants.DOT + + ConfigConstants.FunctionModuleConstants.MANAGER) +public class ManagerAutoConfiguration { +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ManagerSseManager.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ManagerSseManager.java new file mode 100644 index 0000000..1fd6815 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ManagerSseManager.java @@ -0,0 +1,98 @@ +/* + * 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.manager.config; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.ManagerEventTypeEnum; +import org.apache.hertzbeat.common.entity.dto.ImportTaskMessage; +import org.apache.hertzbeat.common.entity.dto.ManagerMessage; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manager SSE + */ +@Slf4j +@Component +public class ManagerSseManager { + private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>(); + + public SseEmitter createEmitter(Long clientId) { + SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); + emitter.onCompletion(() -> removeEmitter(clientId)); + emitter.onTimeout(() -> removeEmitter(clientId)); + emitters.put(clientId, emitter); + return emitter; + } + + @Async + public void broadcast(String eventName, String data) { + emitters.forEach((clientId, emitter) -> { + try { + emitter.send(SseEmitter.event() + .id(String.valueOf(System.currentTimeMillis())) + .name(eventName) + .data(data)); + } catch (IOException | IllegalStateException e) { + tryCompleteAndClean(clientId, emitter); + } catch (Exception exception) { + log.error("Failed to broadcast manager message data to client: {}", exception.getMessage()); + tryCompleteAndClean(clientId, emitter); + } + }); + } + + private void tryCompleteAndClean(Long clientId, SseEmitter emitter) { + try { + Optional.ofNullable(emitter).ifPresent(ResponseBodyEmitter::complete); + } catch (Throwable e) { + log.debug("Failed to complete emitter for client {}: {}", clientId, e.getMessage()); + } + // execute clear + removeEmitter(clientId); + } + + public void broadcastImportTaskInProgress(String taskName, Integer progress){ + ManagerMessage managerMessage = ImportTaskMessage.createInProgressMessage(taskName, progress); + broadcast(ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), JsonUtil.toJson(managerMessage)); + } + + public void broadcastImportTaskSuccess(String taskName){ + ManagerMessage managerMessage = ImportTaskMessage.createCompletedMessage(taskName); + broadcast(ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), JsonUtil.toJson(managerMessage)); + } + + public void broadcastImportTaskFail(String taskName, String errMsg){ + ManagerMessage managerMessage = ImportTaskMessage.createFailedMessage(taskName, errMsg); + broadcast(ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), JsonUtil.toJson(managerMessage)); + } + + private void removeEmitter(Long clientId) { + emitters.remove(clientId); + } +} \ No newline at end of file diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/PrometheusProxyConfig.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/PrometheusProxyConfig.java new file mode 100644 index 0000000..9947034 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/PrometheusProxyConfig.java @@ -0,0 +1,81 @@ +/* + * 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.manager.config; + +import jakarta.annotation.PostConstruct; +import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties; +import org.apache.hertzbeat.warehouse.store.history.tsdb.vm.VictoriaMetricsProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Configuration class for Prometheus Proxy. + * This class determines whether to use PrometheusProxyCollectImpl or PrometheusAutoCollectImpl + * based on the presence of GreptimeDB or VictoriaMetrics properties. + */ +@Component +public class PrometheusProxyConfig { + private static GreptimeProperties staticGreptimeProperties; + private static VictoriaMetricsProperties staticVictoriaMetricsProperties; + + private final GreptimeProperties greptimeProperties; + private final VictoriaMetricsProperties victoriaMetricsProperties; + + /** + * Constructs the factory and injects warehouse properties. + * Uses @Autowired(required = false) to allow these properties to be optional, + * in case they are not configured or enabled in the warehouse module. + * + * @param greptimeProperties GreptimeDB configuration properties. + * @param victoriaMetricsProperties VictoriaMetrics configuration properties. + */ + @Autowired + public PrometheusProxyConfig( + @Autowired(required = false) GreptimeProperties greptimeProperties, + @Autowired(required = false) VictoriaMetricsProperties victoriaMetricsProperties) { + this.greptimeProperties = greptimeProperties; + this.victoriaMetricsProperties = victoriaMetricsProperties; + } + + /** + * Initializes static fields with the injected properties after construction. + * This allows the static getCollector method to access these configurations. + */ + @PostConstruct + private void initStatic() { + staticGreptimeProperties = this.greptimeProperties; + staticVictoriaMetricsProperties = this.victoriaMetricsProperties; + } + + /** + * Judges whether to use PrometheusProxyCollectImpl or PrometheusAutoCollectImpl + */ + public boolean isPrometheusProxy() { + if (staticGreptimeProperties != null && staticGreptimeProperties.enabled()) { + return true; + } + if (staticVictoriaMetricsProperties != null && staticVictoriaMetricsProperties.enabled()) { + return true; + } + return false; + } + + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/RestTemplateConfig.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/RestTemplateConfig.java new file mode 100644 index 0000000..8930dd3 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/RestTemplateConfig.java @@ -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.manager.config; + +import java.net.http.HttpClient; +import java.time.Duration; +import java.util.Collections; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.JdkClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +/** + * RestTemplate configuration using JDK native HttpClient for Spring 7.0. + */ +@Configuration +public class RestTemplateConfig { + + /** + * Create RestTemplate with JDK native request factory and custom interceptors. + */ + @Bean + public RestTemplate restTemplate(ClientHttpRequestFactory factory) { + RestTemplate restTemplate = new RestTemplate(factory); + restTemplate.setInterceptors(Collections.singletonList(new HeaderRequestInterceptor())); + return restTemplate; + } + + @Bean + public ClientHttpRequestFactory clientHttpRequestFactory() { + HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(NetworkConstants.HttpClientConstants.CONNECT_TIME_OUT)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + + JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(httpClient); + + factory.setReadTimeout(Duration.ofSeconds(NetworkConstants.HttpClientConstants.READ_TIME_OUT)); + + return factory; + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SchedulerConfig.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SchedulerConfig.java new file mode 100644 index 0000000..1a8ad42 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SchedulerConfig.java @@ -0,0 +1,40 @@ +/* + * 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.manager.config; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.manager.scheduler.ConsistentHash; +import org.apache.hertzbeat.manager.scheduler.SchedulerProperties; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * scheduler config + */ +@Configuration +@AutoConfigureAfter(value = {SchedulerProperties.class}) +@Slf4j +public class SchedulerConfig { + + @Bean + public ConsistentHash consistentHasInstance() { + return new ConsistentHash(); + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SecurityCorsConfiguration.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SecurityCorsConfiguration.java new file mode 100644 index 0000000..69065ee --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SecurityCorsConfiguration.java @@ -0,0 +1,49 @@ +/* + * 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.manager.config; + +import java.util.Collections; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * SecurityCorsConfiguration class + */ +@Configuration +public class SecurityCorsConfiguration { + + @Bean + public FilterRegistrationBean corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowCredentials(true); + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList(CorsConfiguration.ALL)); + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + source.registerCorsConfiguration("/**", corsConfiguration); + FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(); + bean.setOrder(Integer.MIN_VALUE); + bean.setFilter(new CorsFilter(source)); + bean.addUrlPatterns("/*"); + return bean; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/StatusProperties.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/StatusProperties.java new file mode 100644 index 0000000..7a2d9da --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/StatusProperties.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.config; + +import lombok.Getter; +import lombok.Setter; +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * status page properties + */ + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = ConfigConstants.FunctionModuleConstants.STATUS) +public class StatusProperties { + + /** + * calculate component status properties + */ + private CalculateProperties calculate; + + /** + * calculate component status properties + */ + @Getter + @Setter + public static class CalculateProperties { + + /** + * the component status calculate interval(s) + */ + private Integer interval = 300; + + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SwaggerConfig.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SwaggerConfig.java new file mode 100644 index 0000000..295cbc9 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SwaggerConfig.java @@ -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.manager.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * swagger config + * url: /swagger-ui/index.html + */ +@Configuration +public class SwaggerConfig { + + private static final String SECURITY_SCHEME_NAME = "BearerAuth"; + + @Bean + public OpenAPI springOpenApi() { + return new OpenAPI() + .info(new Info() + .title("HertzBeat") + .description("An Open-Source Real-time Monitoring Tool.") + .termsOfService("https://hertzbeat.apache.org/") + .contact(new Contact().name("tom").url("https://github.com/tomsun28").email("tomsun28@outlook.com")) + .version("v1.0") + .license(new License().name("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0"))) + .externalDocs(new ExternalDocumentation() + .description("HertzBeat Docs").url("https://hertzbeat.apache.org/docs/")) + .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) + .components(new Components().addSecuritySchemes(SECURITY_SCHEME_NAME, + new SecurityScheme() + .name(SECURITY_SCHEME_NAME) + .type(SecurityScheme.Type.HTTP) + .scheme("Bearer") + .bearerFormat("JWT"))); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AccountController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AccountController.java new file mode 100644 index 0000000..1f86394 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AccountController.java @@ -0,0 +1,75 @@ +/* + * 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.manager.controller; + +import static org.apache.hertzbeat.common.constants.CommonConstants.LOGIN_FAILED_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.jsonwebtoken.ExpiredJwtException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.Map; +import javax.naming.AuthenticationException; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.util.ResponseUtil; +import org.apache.hertzbeat.manager.pojo.dto.LoginDto; +import org.apache.hertzbeat.manager.pojo.dto.RefreshTokenResponse; +import org.apache.hertzbeat.manager.pojo.dto.TokenDto; +import org.apache.hertzbeat.manager.service.AccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Authentication registration TOKEN management API + */ +@Tag(name = "Auth Manage API") +@RestController +@RequestMapping(value = "/api/account/auth", produces = {APPLICATION_JSON_VALUE}) +@Slf4j +public class AccountController { + + @Autowired + private AccountService accountService; + + @PostMapping("/form") + @Operation(summary = "Account password login to obtain associated user information", description = "Account password login to obtain associated user information") + public ResponseEntity<Message<Map<String, String>>> authGetToken(@Valid @RequestBody LoginDto loginDto) { + return ResponseUtil.handle(() -> accountService.authGetToken(loginDto)); + } + + @PostMapping("/refresh") + @Operation(summary = "Use refresh TOKEN to re-acquire TOKEN", description = "Use refresh TOKEN to re-acquire TOKEN") + public ResponseEntity<Message<RefreshTokenResponse>> refreshToken(@Valid @RequestBody TokenDto tokenDto) { + try { + return ResponseEntity.ok(Message.success(accountService.refreshToken(tokenDto.getToken()))); + } catch (AuthenticationException e) { + return ResponseEntity.ok(Message.fail(LOGIN_FAILED_CODE, e.getMessage())); + } catch (ExpiredJwtException expiredJwtException) { + log.warn("{}", expiredJwtException.getMessage()); + return ResponseEntity.ok(Message.fail(LOGIN_FAILED_CODE, "Refresh Token Expired")); + } catch (Exception e) { + log.error("Exception occurred during token refresh: {}", e.getClass().getName(), e); + return ResponseEntity.ok(Message.fail(LOGIN_FAILED_CODE, "Refresh Token Error")); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AppController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AppController.java new file mode 100644 index 0000000..96397c5 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AppController.java @@ -0,0 +1,181 @@ +/* + * 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.manager.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.util.ResponseUtil; +import org.apache.hertzbeat.manager.pojo.dto.Hierarchy; +import org.apache.hertzbeat.manager.pojo.dto.MonitorDefineDto; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.apache.hertzbeat.manager.service.AppService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Monitoring Type Management API + */ +@Tag(name = "Monitor Type Manage API") +@RestController +@RequestMapping(path = "/api/apps", produces = {APPLICATION_JSON_VALUE}) +public class AppController { + + private static final String[] RISKY_STR_ARR = {"ScriptEngineManager", "URLClassLoader", "!!", + "ClassLoader", "AnnotationConfigApplicationContext", "FileSystemXmlApplicationContext", + "GenericXmlApplicationContext", "GenericGroovyApplicationContext", "GroovyScriptEngine", + "GroovyClassLoader", "GroovyShell", "ScriptEngine", "ScriptEngineFactory", "XmlWebApplicationContext", + "ClassPathXmlApplicationContext", "MarshalOutputStream", "InflaterOutputStream", "FileOutputStream"}; + + @Autowired + private AppService appService; + + @GetMapping(path = "/{app}/params") + @Operation(summary = "The structure of the input parameters required to specify the monitoring type according to the app query", + description = "The structure of the input parameters required to specify the monitoring type according to the app query") + public ResponseEntity<Message<List<ParamDefineInfo>>> queryAppParamDefines( + @Parameter(description = "en: Monitoring type name", example = "api") @PathVariable("app") final String app) { + return ResponseUtil.handle(() -> appService.getAppParamDefines(app.toLowerCase())); + } + + @GetMapping(path = "/{monitorId}/pushdefine") + @Operation(summary = "The definition structure of the specified monitoring type according to the push query", + description = "The definition structure of the specified monitoring type according to the push query") + public ResponseEntity<Message<Job>> queryPushDefine( + @Parameter(description = "en: Monitoring type name", example = "api") @PathVariable("monitorId") final Long monitorId) { + return ResponseUtil.handle(() -> appService.getPushDefine(monitorId)); + } + + @GetMapping(path = "/{monitorId}/define/dynamic") + @Operation(summary = "The definition structure of the specified monitoring type according to the push query", + description = "The definition structure of the specified monitoring type according to the push query") + public ResponseEntity<Message<Job>> queryAutoGenerateDynamicAppDefine( + @Parameter(description = "Monitoring id", example = "5435345") @PathVariable("monitorId") final Long monitorId) { + return ResponseUtil.handle(() -> appService.getAutoGenerateDynamicDefine(monitorId)); + } + + @GetMapping(path = "/{app}/define") + @Operation(summary = "The definition structure of the specified monitoring type according to the app query", + description = "The definition structure of the specified monitoring type according to the app query") + public ResponseEntity<Message<Job>> queryAppDefine( + @Parameter(description = "en: Monitoring type name", example = "api") @PathVariable("app") final String app) { + return ResponseUtil.handle(() -> appService.getAppDefine(app.toLowerCase())); + } + + @GetMapping(path = "/{app}/define/yml") + @Operation(summary = "The definition yml of the specified monitoring type according to the app query", + description = "The definition yml of the specified monitoring type according to the app query") + public ResponseEntity<Message<String>> queryAppDefineYml( + @Parameter(description = "en: Monitoring type name", example = "api") @PathVariable("app") final String app) { + return ResponseUtil.handle(() -> appService.getMonitorDefineFileContent(app)); + } + + @DeleteMapping(path = "/{app}/define/yml") + @Operation(summary = "Delete monitor define yml", description = "Delete the definition YML for the specified monitoring type according to the app") + public ResponseEntity<Message<Void>> deleteAppDefineYml( + @Parameter(description = "en: Monitoring type name", example = "api") @PathVariable("app") final String app) { + return ResponseUtil.handle(() -> appService.deleteMonitorDefine(app)); + } + + @PostMapping(path = "/define/yml") + @Operation(summary = "Add new monitoring type define yml", description = "Add new monitoring type define yml") + public ResponseEntity<Message<Void>> newAppDefineYml(@Valid @RequestBody MonitorDefineDto defineDto) { + return ResponseUtil.handle(() -> { + for (String riskyToken : RISKY_STR_ARR) { + if (defineDto.getDefine().contains(riskyToken)) { + throw new RuntimeException("can not has malicious remote script"); + } + } + appService.applyMonitorDefineYml(defineDto.getDefine(), false); + }); + } + + @PutMapping(path = "/define/yml") + @Operation(summary = "Update monitoring type define yml", description = "Update monitoring type define yml") + public ResponseEntity<Message<Void>> updateAppDefineYml(@Valid @RequestBody MonitorDefineDto defineDto) { + return ResponseUtil.handle(() -> { + for (String riskyToken : RISKY_STR_ARR) { + if (defineDto.getDefine().contains(riskyToken)) { + throw new RuntimeException("can not has malicious remote script"); + } + } + appService.applyMonitorDefineYml(defineDto.getDefine(), true); + }); + } + + @GetMapping(path = "/hierarchy") + @Operation(summary = "Query all monitor metrics level, output in a hierarchical structure", description = "Query all monitor metrics level, output in a hierarchical structure") + public ResponseEntity<Message<List<Hierarchy>>> queryAppsHierarchy( + @Parameter(description = "en: language type", + example = "zh-CN") + @RequestParam(name = "lang", required = false) String lang) { + String newLang = getLang(lang); + return ResponseUtil.handle(() -> appService.getAllAppHierarchy(newLang)); + } + + @GetMapping(path = "/hierarchy/{app}") + @Operation(summary = "Query all monitor metrics level, output in a hierarchical structure", description = "Query all monitor metrics level, output in a hierarchical structure") + public ResponseEntity<Message<List<Hierarchy>>> queryAppsHierarchyByApp( + @Parameter(description = "en: language type", + example = "zh-CN") + @RequestParam(name = "lang", required = false) String lang, + @Parameter(description = "en: Monitoring type name", example = "api") @PathVariable("app") final String app) { + String newLang = getLang(lang); + return ResponseUtil.handle(() -> appService.getAppHierarchy(app, newLang)); + } + + @GetMapping(path = "/defines") + @Operation(summary = "Query all monitor types", description = "Query all monitor types") + public ResponseEntity<Message<Map<String, String>>> getAllAppDefines( + @Parameter(description = "en: language type", + example = "zh-CN") + @RequestParam(name = "lang", required = false) String lang) { + String newLang = getLang(lang); + return ResponseUtil.handle(() -> appService.getI18nApps(newLang)); + } + + private String getLang(@RequestParam(name = "lang", required = false) @Parameter(description = "en: language type", example = "zh-CN") String lang) { + if (lang == null || lang.isEmpty()) { + lang = "zh-CN"; + } + if (lang.contains(Locale.ENGLISH.getLanguage())) { + lang = "en-US"; + } else if (lang.contains(Locale.CHINESE.getLanguage())) { + lang = "zh-CN"; + } else { + lang = "en-US"; + } + return lang; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AuthTokenController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AuthTokenController.java new file mode 100644 index 0000000..4aa3abb --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AuthTokenController.java @@ -0,0 +1,118 @@ +/* + * 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.manager.controller; + +import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +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 java.util.Collections; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.entity.manager.AuthToken; +import org.apache.hertzbeat.manager.service.AccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * API Token Management Controller + */ +@Tag(name = "API Token Management") +@RestController +@RequestMapping(value = "/api/account/token", produces = {APPLICATION_JSON_VALUE}) +@Slf4j +public class AuthTokenController { + + @Autowired + private AccountService accountService; + + @PostMapping("/generate") + @Operation(summary = "Generate a new API token", description = "Generate a new API token for integrations, optionally with expiration") + public ResponseEntity<Message<Map<String, String>>> generateToken( + @RequestParam(value = "name", required = false) @Parameter(description = "Token name/description") String name, + @RequestParam(value = "expireSeconds", required = false) @Parameter(description = "Expiration time in seconds, null means never expire") Long expireSeconds) { + SubjectSum subjectSum = SurenessContextHolder.getBindSubject(); + if (subjectSum == null) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "No login user")); + } + if (!subjectSum.hasRole("admin")) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "No permission")); + } + try { + String token = accountService.generateToken(name, expireSeconds); + Map<String, String> rep = Collections.singletonMap("token", token); + return ResponseEntity.ok(Message.success(rep)); + } catch (Exception e) { + log.error("generate token error", e); + return ResponseEntity.ok(Message.fail(FAIL_CODE, "Generate token error")); + } + } + + @GetMapping + @Operation(summary = "List all API tokens", description = "List all active non-expiring API tokens") + public ResponseEntity<Message<List<AuthToken>>> listTokens() { + SubjectSum subjectSum = SurenessContextHolder.getBindSubject(); + if (subjectSum == null) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "No login user")); + } + if (!subjectSum.hasRole("admin")) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "No permission")); + } + try { + List<AuthToken> tokens = accountService.listTokens(); + return ResponseEntity.ok(Message.success(tokens)); + } catch (Exception e) { + log.error("list tokens error", e); + return ResponseEntity.ok(Message.fail(FAIL_CODE, "List tokens error")); + } + } + + @DeleteMapping("/{id}") + @Operation(summary = "Delete/revoke an API token", description = "Delete an API token to revoke its access") + public ResponseEntity<Message<Void>> deleteToken( + @PathVariable("id") @Parameter(description = "Token ID") Long id) { + SubjectSum subjectSum = SurenessContextHolder.getBindSubject(); + if (subjectSum == null) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "No login user")); + } + if (!subjectSum.hasRole("admin")) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "No permission")); + } + try { + accountService.deleteToken(id); + return ResponseEntity.ok(Message.success(null)); + } catch (javax.naming.AuthenticationException e) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "No permission")); + } catch (Exception e) { + log.error("delete token error", e); + return ResponseEntity.ok(Message.fail(FAIL_CODE, "Delete token error")); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/BulletinController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/BulletinController.java new file mode 100644 index 0000000..3c8bddf --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/BulletinController.java @@ -0,0 +1,117 @@ +/* + * 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.manager.controller; + +import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.entity.manager.Bulletin; +import org.apache.hertzbeat.manager.pojo.dto.BulletinMetricsData; +import org.apache.hertzbeat.common.util.ResponseUtil; +import org.apache.hertzbeat.manager.service.BulletinService; +import org.apache.hertzbeat.warehouse.store.realtime.RealTimeDataReader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Bulletin Controller + */ +@Slf4j +@RestController +@RequestMapping(value = "/api/bulletin", produces = {APPLICATION_JSON_VALUE}) +public class BulletinController { + + @Autowired + private BulletinService bulletinService; + + @Autowired + private RealTimeDataReader realTimeDataReader; + + @Operation(summary = "New a bulletin", description = "Add new bulletin") + @PostMapping + public ResponseEntity<Message<Void>> addNewBulletin(@Valid @RequestBody Bulletin bulletin) { + try { + bulletinService.validate(bulletin); + bulletinService.addBulletin(bulletin); + } catch (Exception e) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "Add failed! " + e.getMessage())); + } + return ResponseEntity.ok(Message.success("Add success!")); + } + + @Operation(summary = "Update a bulletin", description = "Update the bulletin") + @PutMapping + public ResponseEntity<Message<Void>> editBulletin(@Valid @RequestBody Bulletin bulletin) { + try { + bulletinService.validate(bulletin); + bulletinService.editBulletin(bulletin); + } catch (Exception e) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "Edit failed! " + e.getMessage())); + } + return ResponseEntity.ok(Message.success("Add success!")); + } + + @Operation(summary = "Query One Bulletin", description = "Query One Bulletin") + @GetMapping("/{id}") + public ResponseEntity<Message<Bulletin>> getBulletin(@Valid @PathVariable Long id) { + return ResponseEntity.ok(Message.success(bulletinService.getBulletinById(id).orElse(null))); + } + + @Operation(summary = "Query Bulletins", description = "Query All Bulletin") + @GetMapping + public ResponseEntity<Message<Page<Bulletin>>> queryBulletins( + @Parameter(description = "Search", example = "tom") @RequestParam(required = false) final String search, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") Integer pageIndex, + @Parameter(description = "Number of list pagination", example = "8") @RequestParam(required = false) Integer pageSize) { + return ResponseUtil.handle(() -> bulletinService.getBulletins(search, pageIndex, pageSize)); + } + + @Operation(summary = "Delete Bulletins", description = "Delete Bulletin by ids") + @DeleteMapping + public ResponseEntity<Message<Void>> deleteBulletin(@Parameter(description = "Bulletin ids") @RequestParam List<Long> ids) { + bulletinService.deleteBulletins(ids); + return ResponseEntity.ok(Message.success("Delete success!")); + } + + @GetMapping("/metrics") + @Operation(summary = "Query All Bulletin Real Time Metrics Data", description = "Query All Bulletin real-time metrics data of monitoring indicators") + public ResponseEntity<Message<?>> getAllMetricsData(@RequestParam(name = "id") Long id) { + if (!realTimeDataReader.isServerAvailable()) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "real time store not available")); + } + BulletinMetricsData data = bulletinService.buildBulletinMetricsData(id); + return ResponseEntity.ok(Message.success(data)); + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/CollectorController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/CollectorController.java new file mode 100644 index 0000000..1c921e7 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/CollectorController.java @@ -0,0 +1,98 @@ +/* + * 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.manager.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import java.util.Map; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.util.ResponseUtil; +import org.apache.hertzbeat.manager.pojo.dto.CollectorSummary; +import org.apache.hertzbeat.manager.service.CollectorService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * collector API + */ +@Tag(name = "Collector Manage API") +@RestController() +@RequestMapping(value = "/api/collector", produces = {APPLICATION_JSON_VALUE}) +public class CollectorController { + + @Autowired + private CollectorService collectorService; + + @GetMapping + @Operation(summary = "Get a list of collectors based on query filter items", + description = "Get a list of collectors based on query filter items") + public ResponseEntity<Message<Page<CollectorSummary>>> getCollectors( + @Parameter(description = "collector name", example = "tom") @RequestParam(required = false) final String name, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pagination", example = "8") @RequestParam(required = false) Integer pageSize) { + return ResponseUtil.handle(() -> collectorService.getCollectors(name, pageIndex, pageSize)); + } + + @PutMapping("/online") + @Operation(summary = "Online collectors") + public ResponseEntity<Message<Void>> onlineCollector( + @Parameter(description = "collector name", example = "demo-collector") + @RequestParam(required = false) List<String> collectors) { + collectorService.makeCollectorsOnline(collectors); + return ResponseEntity.ok(Message.success("Online success")); + } + + @PutMapping("/offline") + @Operation(summary = "Offline collectors") + public ResponseEntity<Message<Void>> offlineCollector( + @Parameter(description = "collector name", example = "demo-collector") + @RequestParam(required = false) List<String> collectors) { + collectorService.makeCollectorsOffline(collectors); + return ResponseEntity.ok(Message.success("Offline success")); + } + + @DeleteMapping + @Operation(summary = "Delete collectors") + public ResponseEntity<Message<Void>> deleteCollector( + @Parameter(description = "collector name", example = "demo-collector") + @RequestParam(required = false) List<String> collectors) { + this.collectorService.deleteRegisteredCollector(collectors); + return ResponseEntity.ok(Message.success("Delete success")); + } + + @PostMapping("/generate/{collector}") + @Operation(summary = "Generate deploy collector info") + public ResponseEntity<Message<Map<String, String>>> generateCollectorDeployInfo( + @Parameter(description = "collector name", example = "demo-collector") + @PathVariable() String collector) { + return ResponseUtil.handle(() -> collectorService.generateCollectorDeployInfo(collector)); + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/GeneralConfigController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/GeneralConfigController.java new file mode 100644 index 0000000..5d36543 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/GeneralConfigController.java @@ -0,0 +1,117 @@ +/* + * 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.manager.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.ResponseUtil; +import org.apache.hertzbeat.manager.pojo.dto.TemplateConfig; +import org.apache.hertzbeat.manager.service.ConfigService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.TextStyle; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +/** + * Generate Configuration API + */ +@RestController +@RequestMapping(value = "/api/config", produces = {APPLICATION_JSON_VALUE}) +@Tag(name = "Generate Configuration API") +@Slf4j +public class GeneralConfigController { + + private static final Set<String> ZONE_IDS = ZoneId.getAvailableZoneIds(); + + @Resource + private ConfigService configService; + + + @PostMapping(path = "/{type}") + @Operation(summary = "Save or update common config", description = "Save or update common config") + public ResponseEntity<Message<String>> saveOrUpdateConfig( + @Parameter(description = "Config Type", example = "email") + @PathVariable("type") @NotNull final String type, + @RequestBody Object config) { + configService.saveConfig(type, config); + return ResponseEntity.ok(Message.success("Update config success")); + } + + @GetMapping(path = "/{type}") + @Operation(summary = "Get the sender config", description = "Get the sender config") + public ResponseEntity<Message<Object>> getConfig( + @Parameter(description = "Config Type", example = "email") + @PathVariable("type") @NotNull final String type) { + return ResponseUtil.handle(() -> configService.getConfig(type)); + } + + @PutMapping(path = "/template/{app}") + @Operation(summary = "Update the app template config") + public ResponseEntity<Message<Void>> updateTemplateAppConfig( + @PathVariable("app") @NotNull final String app, + @RequestBody TemplateConfig.AppTemplate template) { + return ResponseUtil.handle(() -> configService.updateTemplateAppConfig(app, template)); + } + + @GetMapping(path = "/timezones") + @Operation(summary = "Get all available timezones and their current UTC offset", description = "Get all available timezones and their current UTC offset") + public ResponseEntity<Message<List<Map<String, String>>>> getTimezones() { + List<Map<String, String>> timezones = ZONE_IDS.stream() + .map(id -> { + try { + ZoneId zoneId = ZoneId.of(id); + ZonedDateTime now = ZonedDateTime.now(zoneId); + int totalSeconds = now.getOffset().getTotalSeconds(); + String offset = String.format("UTC%+03d:%02d", totalSeconds / 3600, Math.abs((totalSeconds / 60) % 60)); + String displayName = zoneId.getDisplayName(TextStyle.FULL, Locale.getDefault()); + return Map.of("zoneId", id, "offset", offset, "displayName", displayName); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn("Query Timezone failed. {} ", errorMsg); + return null; + } + }) + .filter(t -> Objects.nonNull(t) && Objects.nonNull(t.get("zoneId"))) + .sorted(Comparator.comparing(m -> m.get("zoneId"))) + .collect(Collectors.toList()); + return ResponseEntity.ok(Message.success(timezones)); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/I18nController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/I18nController.java new file mode 100644 index 0000000..dd39f9b --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/I18nController.java @@ -0,0 +1,58 @@ +/* + * 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.manager.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Map; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.manager.service.AppService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Internationalization I18N + */ +@Tag(name = "I18N International Resource API") +@RestController +@RequestMapping(path = "/api/i18n", produces = {APPLICATION_JSON_VALUE}) +public class I18nController { + + @Autowired + private AppService appService; + + @GetMapping("/{lang}") + @Operation(summary = "Query total i 18 n internationalized text resources", description = "Query total i18n internationalized text resources") + public ResponseEntity<Message<Map<String, String>>> queryI18n( + @Parameter(description = "en: language type", example = "zh-CN") + @PathVariable(name = "lang", required = false) String lang) { + if (lang == null || lang.isEmpty()) { + lang = "zh-CN"; + } + lang = "zh-cn".equalsIgnoreCase(lang) || "zh_cn".equalsIgnoreCase(lang) ? "zh-CN" : lang; + lang = "en-us".equalsIgnoreCase(lang) || "en_us".equalsIgnoreCase(lang) ? "en-US" : lang; + Map<String, String> i18nResource = appService.getI18nResources(lang); + return ResponseEntity.ok(Message.success(i18nResource)); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/LabelController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/LabelController.java new file mode 100644 index 0000000..eba5c55 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/LabelController.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.entity.manager.Label; +import org.apache.hertzbeat.base.service.LabelService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Label management API + */ +@Tag(name = "Label Manage API") +@RestController +@RequestMapping(path = "/api/label", produces = {APPLICATION_JSON_VALUE}) +public class LabelController { + + @Autowired + private LabelService labelService; + + @PostMapping + @Operation(summary = "Add Label", description = "Add Label") + public ResponseEntity<Message<Void>> addNewLabels(@Valid @RequestBody Label label) { + labelService.addLabel(label); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping + @Operation(summary = "Modify an existing Label", description = "Modify an existing Label") + public ResponseEntity<Message<Void>> modifyLabel(@Valid @RequestBody Label label) { + if (label.getId() == null) { + throw new IllegalArgumentException("ID cannot be null."); + } + labelService.modifyLabel(label); + return ResponseEntity.ok(Message.success("Modify success")); + } + + @GetMapping() + @Operation(summary = "Get labels information", description = "Obtain label information based on conditions") + public ResponseEntity<Message<Page<Label>>> getLabels( + @Parameter(description = "Label content search", example = "status") @RequestParam(required = false) String search, + @Parameter(description = "Label type", example = "0") @RequestParam(required = false) Byte type, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pagination", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + return ResponseEntity.ok(Message.success(labelService.getLabels(search, type, pageIndex, pageSize))); + } + + @DeleteMapping() + @Operation(summary = "Delete Label based on ID", description = "Delete Label based on ID") + public ResponseEntity<Message<Void>> deleteLabels( + @Parameter(description = "Label id ", example = "6565463543") @RequestParam(required = false) List<Long> ids) { + labelService.deleteLabels(new HashSet<>(ids)); + return ResponseEntity.ok(Message.success("Delete success")); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/ManagerSseController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/ManagerSseController.java new file mode 100644 index 0000000..5c7de13 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/ManagerSseController.java @@ -0,0 +1,50 @@ +/* + * 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.manager.controller; + +import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator; +import org.apache.hertzbeat.manager.config.ManagerSseManager; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE; + + +/** + * SSE controller for manager + */ +@RestController +@RequestMapping(path = "/api/manager/sse", produces = {TEXT_EVENT_STREAM_VALUE}) +public class ManagerSseController { + + private final ManagerSseManager emitterManager; + + public ManagerSseController(ManagerSseManager emitterManager) { + this.emitterManager = emitterManager; + } + + @GetMapping(path = "/subscribe") + public SseEmitter subscribe() { + Long clientId = SnowFlakeIdGenerator.generateId(); + return emitterManager.createEmitter(clientId); + } +} \ No newline at end of file diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MetricsController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MetricsController.java new file mode 100644 index 0000000..712c75b --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MetricsController.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.HashMap; +import java.util.Map; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.common.queue.impl.InMemoryCommonDataQueue; +import org.springframework.beans.factory.annotation.Autowired; +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; + +/** + * hertzbeat metrics exporter + */ +@Tag(name = "Inner Metrics Exporter API") +@RestController +@RequestMapping(path = "/api/metrics", produces = {APPLICATION_JSON_VALUE}) +public class MetricsController { + + @Autowired + private CommonDataQueue commonDataQueue; + + @GetMapping() + @Operation(summary = "Get Hertzbeat Metrics Data") + public ResponseEntity<Message<Map<String, Object>>> getMetricsInfo() { + Map<String, Object> metricsInfo = new HashMap<>(8); + if (commonDataQueue instanceof InMemoryCommonDataQueue dataQueue) { + Map<String, Integer> queueInfo = dataQueue.getQueueSizeMetricsInfo(); + metricsInfo.putAll(queueInfo); + } + return ResponseEntity.ok(Message.success(metricsInfo)); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MetricsFavoriteController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MetricsFavoriteController.java new file mode 100644 index 0000000..be3950f --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MetricsFavoriteController.java @@ -0,0 +1,105 @@ +/* + * 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.manager.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.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.manager.service.MetricsFavoriteService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Set; + +import static org.apache.hertzbeat.common.constants.CommonConstants.LOGIN_FAILED_CODE; + +/** + * Metrics Favorite Controller + */ +@Tag(name = "Metrics Favorite API") +@RestController +@RequestMapping(path = "/api/metrics/favorite") +@RequiredArgsConstructor +@Slf4j +public class MetricsFavoriteController { + + private final MetricsFavoriteService metricsFavoriteService; + + @PostMapping("/{monitorId}/{metricsName}") + @Operation(summary = "Add metrics to favorites", description = "Add specific metrics to user's favorites") + public ResponseEntity<Message<Void>> addMetricsFavorite( + @Parameter(description = "Monitor ID", example = "6565463543") @PathVariable Long monitorId, + @Parameter(description = "Metrics name", example = "cpu") @PathVariable String metricsName) { + String user = getCurrentUser(); + if (user == null) { + return ResponseEntity.ok(Message.fail(LOGIN_FAILED_CODE, "User not authenticated")); + } + metricsFavoriteService.addMetricsFavorite(user, monitorId, metricsName); + return ResponseEntity.ok(Message.success("Metrics added to favorites successfully")); + } + + @DeleteMapping("/{monitorId}/{metricsName}") + @Operation(summary = "Remove metrics from favorites", description = "Remove specific metrics from user's favorites") + public ResponseEntity<Message<Void>> removeMetricsFavorite( + @Parameter(description = "Monitor ID", example = "6565463543") @PathVariable Long monitorId, + @Parameter(description = "Metrics name", example = "cpu") @PathVariable String metricsName) { + + String user = getCurrentUser(); + if (user == null) { + return ResponseEntity.ok(Message.fail(LOGIN_FAILED_CODE, "User not authenticated")); + } + metricsFavoriteService.removeMetricsFavorite(user, monitorId, metricsName); + return ResponseEntity.ok(Message.success("Metrics removed from favorites successfully")); + } + + @GetMapping("/{monitorId}") + @Operation(summary = "Get user's all favorited metrics", description = "Get all favorited metrics for current user") + public ResponseEntity<Message<Set<String>>> getUserFavoritedMetrics(@Parameter(description = "Monitor ID", example = "6565463543") @PathVariable Long monitorId) { + String user = getCurrentUser(); + if (user == null) { + return ResponseEntity.ok(Message.fail(LOGIN_FAILED_CODE, "User not authenticated")); + } + Set<String> favoritedMetrics = metricsFavoriteService.getUserFavoritedMetrics(user, monitorId); + return ResponseEntity.ok(Message.success(favoritedMetrics)); + } + + /** + * Get current user ID for favorite status + * + * @return user id + */ + private String getCurrentUser() { + try { + SubjectSum subjectSum = SurenessContextHolder.getBindSubject(); + return String.valueOf(subjectSum.getPrincipal()); + } catch (Exception e) { + log.error("No user found, favorites will be disabled"); + return null; + } + } +} \ No newline at end of file diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorController.java new file mode 100644 index 0000000..e44ac75 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorController.java @@ -0,0 +1,117 @@ +/* + * 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.manager.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.manager.pojo.dto.MonitorDto; +import org.apache.hertzbeat.manager.service.MonitorService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE; +import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +/** + * Monitoring management API + */ +@Tag(name = "Monitor Manage API") +@RestController +@RequestMapping(path = "/api/monitor", produces = {APPLICATION_JSON_VALUE}) +public class MonitorController { + + @Autowired + private MonitorService monitorService; + + @PostMapping + @Operation(summary = "Add a monitoring application", description = "Add a monitoring application") + public ResponseEntity<Message<Void>> addNewMonitor(@Valid @RequestBody MonitorDto monitorDto) { + // Verify request data + monitorService.validate(monitorDto, false); + monitorService.addMonitor(monitorDto.getMonitor(), monitorDto.getParams(), monitorDto.getCollector(), monitorDto.getGrafanaDashboard()); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping + @Operation(summary = "Modify an existing monitoring application", description = "Modify an existing monitoring application") + public ResponseEntity<Message<Void>> modifyMonitor(@Valid @RequestBody MonitorDto monitorDto) { + // Verify request data + monitorService.validate(monitorDto, true); + monitorService.modifyMonitor(monitorDto.getMonitor(), monitorDto.getParams(), monitorDto.getCollector(), monitorDto.getGrafanaDashboard()); + return ResponseEntity.ok(Message.success("Modify success")); + } + + @GetMapping(path = "/{id}") + @Operation(summary = "Obtain monitoring information based on monitoring ID", description = "Obtain monitoring information based on monitoring ID") + public ResponseEntity<Message<MonitorDto>> getMonitor( + @Parameter(description = "Monitoring task ID", example = "6565463543") @PathVariable("id") final long id) { + // Get monitoring information + MonitorDto monitorDto = monitorService.getMonitorDto(id); + if (monitorDto == null) { + return ResponseEntity.ok(Message.fail(MONITOR_NOT_EXIST_CODE, "Monitor not exist.")); + } else { + return ResponseEntity.ok(Message.success(monitorDto)); + } + } + + @DeleteMapping(path = "/{id}") + @Operation(summary = "Delete monitoring application based on monitoring ID", description = "Delete monitoring application based on monitoring ID") + public ResponseEntity<Message<Void>> deleteMonitor( + @Parameter(description = "en: Monitor ID", example = "6565463543") @PathVariable("id") final long id) { + // delete monitor + Monitor monitor = monitorService.getMonitor(id); + if (monitor == null) { + return ResponseEntity.ok(Message.success("The specified monitoring was not queried, please check whether the parameters are correct")); + } + monitorService.deleteMonitor(id); + return ResponseEntity.ok(Message.success("Delete success")); + } + + @PostMapping(path = "/detect") + @Operation(summary = "Perform availability detection on this monitoring based on monitoring information", + description = "Perform availability detection on this monitoring based on monitoring information") + public ResponseEntity<Message<Void>> detectMonitor(@Valid @RequestBody MonitorDto monitorDto) { + monitorService.validate(monitorDto, null); + monitorService.detectMonitor(monitorDto.getMonitor(), monitorDto.getParams(), monitorDto.getCollector()); + return ResponseEntity.ok(Message.success("Detect success.")); + } + + @PostMapping("/copy/{id}") + @Operation(summary = "Copy Monitor", description = "Copy an existing monitor") + public ResponseEntity<Message<Void>> copyMonitor(@PathVariable("id") final Long id) { + try { + monitorService.copyMonitor(id); + return ResponseEntity.ok(Message.success("Copy monitor success")); + } catch (Exception e) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "Copy monitor failed: " + e.getMessage())); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorsController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorsController.java new file mode 100644 index 0000000..5ba489e --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MonitorsController.java @@ -0,0 +1,144 @@ +/* + * 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.manager.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.manager.pojo.dto.MonitorInfo; +import org.apache.hertzbeat.manager.service.MonitorService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +/** + * Monitor and manage batch API + */ +@Tag(name = "Monitor Manage Batch API") +@RestController +@RequestMapping(path = "/api/monitors", produces = {APPLICATION_JSON_VALUE}) +public class MonitorsController { + + @Autowired + private MonitorService monitorService; + + @GetMapping + @Operation(summary = "Obtain a list of monitoring information based on query filter items", + description = "Obtain a list of monitoring information based on query filter items") + public ResponseEntity<Message<Page<MonitorInfo>>> getMonitors( + @Parameter(description = "Monitor ID", example = "6565463543") @RequestParam(required = false) final List<Long> ids, + @Parameter(description = "Monitor Type", example = "linux") @RequestParam(required = false) final String app, + @Parameter(description = "Monitor Status 0:no monitor,1:usable,2:disabled,9:all status", example = "1") @RequestParam(required = false) final Byte status, + @Parameter(description = "Monitor Host support fuzzy query", example = "127.0.0.1") @RequestParam(required = false) final String search, + @Parameter(description = "Monitor labels ", example = "env:prod,instance:22") @RequestParam(required = false) final String labels, + @Parameter(description = "Sort Field ", example = "name") @RequestParam(defaultValue = "gmtCreate") final String sort, + @Parameter(description = "Sort mode eg:asc desc", example = "desc") @RequestParam(defaultValue = "desc") final String order, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pagination ", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + Page<Monitor> monitorPage = monitorService.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels); + Page<MonitorInfo> responsePage = monitorPage == null ? Page.empty() : monitorPage.map(MonitorInfo::fromEntity); + return ResponseEntity.ok(Message.success(responsePage)); + } + + @GetMapping(path = "/{app}") + @Operation(summary = "Filter all acquired monitoring information lists of the specified monitoring type according to the query", + description = "Filter all acquired monitoring information lists of the specified monitoring type according to the query") + public ResponseEntity<Message<List<MonitorInfo>>> getAppMonitors( + @Parameter(description = "en: Monitoring type", example = "linux") @PathVariable(required = false) final String app) { + List<Monitor> monitors = monitorService.getAppMonitors(app); + List<MonitorInfo> response = monitors == null ? Collections.emptyList() + : monitors.stream().map(MonitorInfo::fromEntity).toList(); + return ResponseEntity.ok(Message.success(response)); + } + + + @DeleteMapping + @Operation(summary = "Delete monitoring items in batches according to the monitoring ID list", + description = "Delete monitoring items in batches according to the monitoring ID list") + public ResponseEntity<Message<Void>> deleteMonitors( + @Parameter(description = "Monitoring ID List", example = "6565463543") @RequestParam(required = false) List<Long> ids + ) { + if (ids != null && !ids.isEmpty()) { + monitorService.deleteMonitors(new HashSet<>(ids)); + } + return ResponseEntity.ok(Message.success()); + } + + @DeleteMapping("manage") + @Operation(summary = "Unmanaged monitoring items in batches according to the monitoring ID list", + description = "Unmanaged monitoring items in batches according to the monitoring ID list") + public ResponseEntity<Message<Void>> cancelManageMonitors( + @Parameter(description = "Monitoring ID List", example = "6565463543") @RequestParam(required = false) List<Long> ids + ) { + if (ids != null && !ids.isEmpty()) { + monitorService.cancelManageMonitors(new HashSet<>(ids)); + } + return ResponseEntity.ok(Message.success()); + } + + @GetMapping("manage") + @Operation(summary = "Start the managed monitoring items in batches according to the monitoring ID list", + description = "Start the managed monitoring items in batches according to the monitoring ID list") + public ResponseEntity<Message<Void>> enableManageMonitors( + @Parameter(description = "Monitor ID List", example = "6565463543") @RequestParam(required = false) List<Long> ids + ) { + if (ids != null && !ids.isEmpty()) { + monitorService.enableManageMonitors(new HashSet<>(ids)); + } + return ResponseEntity.ok(Message.success()); + } + + @GetMapping("/export") + @Operation(summary = "export monitor config", description = "export monitor config") + public void export( + @Parameter(description = "Monitor ID List", example = "6565463543") @RequestParam List<Long> ids, + @Parameter(description = "Export Type:JSON,EXCEL") @RequestParam(defaultValue = "JSON") String type, + HttpServletResponse res) throws Exception { + monitorService.export(ids, type, res); + } + + @GetMapping("/export/all") + @Operation(summary = "export all monitor config", description = "export all monitor config") + public void exportAll( + @Parameter(description = "Export Type:JSON,EXCEL,YAML") @RequestParam(defaultValue = "JSON") String type, + HttpServletResponse res) throws Exception { + monitorService.exportAll(type, res); + } + + @PostMapping("/import") + @Operation(summary = "import monitor config", description = "import monitor config") + public ResponseEntity<Message<Void>> export(MultipartFile file) throws Exception { + monitorService.importConfig(file); + return ResponseEntity.ok(Message.success("Import success")); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/PluginController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/PluginController.java new file mode 100644 index 0000000..35d187a --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/PluginController.java @@ -0,0 +1,105 @@ +/* + * 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.manager.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import java.util.HashSet; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.entity.manager.PluginMetadata; +import org.apache.hertzbeat.manager.pojo.dto.PluginUpload; +import org.apache.hertzbeat.manager.pojo.dto.PluginParam; +import org.apache.hertzbeat.manager.pojo.dto.PluginParametersVO; +import org.apache.hertzbeat.manager.service.PluginService; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * plugin management API + */ +@io.swagger.v3.oas.annotations.tags.Tag(name = "Plugin Manage API") +@RestController +@RequestMapping(path = "/api/plugin", produces = {APPLICATION_JSON_VALUE}) +@RequiredArgsConstructor +public class PluginController { + + private final PluginService pluginService; + + @PostMapping + @Operation(summary = "upload plugin", description = "upload plugin") + public ResponseEntity<Message<Void>> uploadNewPlugin(@Valid PluginUpload pluginUpload) { + pluginService.savePlugin(pluginUpload); + return ResponseEntity.ok(Message.success("Add success")); + } + + + @GetMapping() + @Operation(summary = "Get Plugins information", description = "Obtain plugins information based on conditions") + public ResponseEntity<Message<Page<PluginMetadata>>> getPlugins( + @Parameter(description = "plugin name search", example = "status") @RequestParam(required = false) String search, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pagination", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + Page<PluginMetadata> alertPage = pluginService.getPlugins(search, pageIndex, pageSize); + return ResponseEntity.ok(Message.success(alertPage)); + } + + @DeleteMapping() + @Operation(summary = "Delete plugins based on ID", description = "Delete plugins based on ID") + public ResponseEntity<Message<Void>> deletePlugins( + @Parameter(description = "Plugin IDs ", example = "6565463543") @RequestParam(required = false) List<Long> ids) { + if (ids != null && !ids.isEmpty()) { + pluginService.deletePlugins(new HashSet<>(ids)); + } + return ResponseEntity.ok(Message.success("Delete success")); + } + + + @PutMapping() + @Operation(summary = "Update enable status", description = "Delete plugins based on ID") + public ResponseEntity<Message<Void>> updatePluginStatus(@RequestBody PluginMetadata plugin) { + pluginService.updateStatus(plugin); + return ResponseEntity.ok(Message.success("Update success")); + } + + @GetMapping("/params/define") + @Operation(summary = "get param define", description = "get param define by jar path") + public ResponseEntity<Message<PluginParametersVO>> getParamDefine(@RequestParam Long pluginMetadataId) { + PluginParametersVO plugins = pluginService.getParamDefine(pluginMetadataId); + return ResponseEntity.ok(Message.success(plugins)); + } + + @PostMapping("/params") + @Operation(summary = "get param define", description = "get param define by jar path") + public ResponseEntity<Message<Boolean>> saveParams(@RequestBody List<PluginParam> pluginParams) { + pluginService.savePluginParam(pluginParams); + return ResponseEntity.ok(Message.success(true)); + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/StatusPageController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/StatusPageController.java new file mode 100644 index 0000000..5664de7 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/StatusPageController.java @@ -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.manager.controller; + +import io.swagger.v3.oas.annotations.Parameter; +import org.springframework.data.domain.Page; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.util.List; + +import jakarta.validation.Valid; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageComponentInfo; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageIncidentInfo; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageOrgInfo; +import org.apache.hertzbeat.manager.service.StatusPageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * status page endpoint controller + */ +@Tag(name = "Status Page API") +@RestController() +@RequestMapping(value = "/api/status/page", produces = {APPLICATION_JSON_VALUE}) +public class StatusPageController { + + @Autowired + private StatusPageService statusPageService; + + @GetMapping("/org") + @Operation(summary = "Query Status Page Organization") + public ResponseEntity<Message<StatusPageOrgInfo>> queryStatusPageOrg() { + StatusPageOrgInfo statusPageOrg = statusPageService.queryStatusPageOrg(); + if (statusPageOrg == null) { + return ResponseEntity.ok(Message.fail(CommonConstants.FAIL_CODE, "Status Page Organization Not Found")); + } + return ResponseEntity.ok(Message.success(statusPageOrg)); + } + + @PostMapping("/org") + @Operation(summary = "Save and Update Query Status Page Organization") + public ResponseEntity<Message<StatusPageOrgInfo>> saveStatusPageOrg(@Valid @RequestBody StatusPageOrgInfo statusPageOrg) { + StatusPageOrgInfo org = statusPageService.saveStatusPageOrg(statusPageOrg); + return ResponseEntity.ok(Message.success(org)); + } + + @GetMapping("/component") + @Operation(summary = "Query Status Page Components") + public ResponseEntity<Message<List<StatusPageComponentInfo>>> queryStatusPageComponent() { + return ResponseEntity.ok(Message.success(statusPageService.queryStatusPageComponents())); + } + + @PostMapping("/component") + @Operation(summary = "Save Status Page Component") + public ResponseEntity<Message<Void>> newStatusPageComponent(@Valid @RequestBody StatusPageComponentInfo statusPageComponent) { + statusPageService.newStatusPageComponent(statusPageComponent); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping("/component") + @Operation(summary = "Update Status Page Component") + public ResponseEntity<Message<Void>> updateStatusPageComponent(@Valid @RequestBody StatusPageComponentInfo statusPageComponent) { + statusPageService.updateStatusPageComponent(statusPageComponent); + return ResponseEntity.ok(Message.success("Update success")); + } + + @DeleteMapping("/component/{id}") + @Operation(summary = "Delete Status Page Component") + public ResponseEntity<Message<Void>> deleteStatusPageComponent(@PathVariable("id") final long id) { + statusPageService.deleteStatusPageComponent(id); + return ResponseEntity.ok(Message.success("Delete success")); + } + + @GetMapping("/component/{id}") + @Operation(summary = "Query Status Page Component") + public ResponseEntity<Message<StatusPageComponentInfo>> queryStatusPageComponent(@PathVariable("id") final long id) { + return ResponseEntity.ok(Message.success(statusPageService.queryStatusPageComponent(id))); + } + + @PostMapping("/incident") + @Operation(summary = "Save Status Page Incident") + public ResponseEntity<Message<Void>> newStatusPageIncident(@Valid @RequestBody StatusPageIncidentInfo incident) { + statusPageService.newStatusPageIncident(incident); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping("/incident") + @Operation(summary = "Update Status Page Incident") + public ResponseEntity<Message<Void>> updateStatusPageIncident(@Valid @RequestBody StatusPageIncidentInfo incident) { + statusPageService.updateStatusPageIncident(incident); + return ResponseEntity.ok(Message.success("Update success")); + } + + @DeleteMapping("/incident/{id}") + @Operation(summary = "Delete Status Page Incident") + public ResponseEntity<Message<Void>> deleteStatusPageIncident(@PathVariable("id") final long id) { + statusPageService.deleteStatusPageIncident(id); + return ResponseEntity.ok(Message.success("Delete success")); + } + + @GetMapping("/incident/{id}") + @Operation(summary = "Get Status Page Incident") + public ResponseEntity<Message<StatusPageIncidentInfo>> queryStatusPageIncident(@PathVariable("id") final long id) { + return ResponseEntity.ok(Message.success(statusPageService.queryStatusPageIncident(id))); + } + + @GetMapping("/incident") + @Operation(summary = "Query Status Page Incidents") + public ResponseEntity<Message<Page<StatusPageIncidentInfo>>> queryStatusPageIncident( + @Parameter(description = "Search-Target", example = "x") @RequestParam(required = false) String search, + @Parameter(description = "Start Time", example = "1756384301907") @RequestParam(required = false) Long startTime, + @Parameter(description = "End Time", example = "1756384301907") @RequestParam(required = false) Long endTime, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pages", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + Page<StatusPageIncidentInfo> incidents = statusPageService.queryStatusPageIncidents(search, startTime, endTime, pageIndex, pageSize); + return ResponseEntity.ok(Message.success(incidents)); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/StatusPagePublicController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/StatusPagePublicController.java new file mode 100644 index 0000000..9a13776 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/StatusPagePublicController.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.controller; + +import io.swagger.v3.oas.annotations.Parameter; +import org.springframework.data.domain.Page; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.util.List; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.manager.pojo.dto.ComponentStatus; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageIncidentInfo; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageOrgInfo; +import org.apache.hertzbeat.manager.service.StatusPageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * status page public endpoint controller + */ +@Tag(name = "Status Page Public API") +@RestController() +@RequestMapping(value = "/api/status/page/public", produces = {APPLICATION_JSON_VALUE}) +public class StatusPagePublicController { + + @Autowired + private StatusPageService statusPageService; + + @GetMapping("/org") + @Operation(summary = "Query Status Page Organization") + public ResponseEntity<Message<StatusPageOrgInfo>> queryStatusPageOrg() { + StatusPageOrgInfo statusPageOrg = statusPageService.queryStatusPageOrg(); + if (statusPageOrg == null) { + return ResponseEntity.ok(Message.fail(CommonConstants.FAIL_CODE, "Status Page Organization Not Found")); + } + return ResponseEntity.ok(Message.success(statusPageOrg)); + } + + @GetMapping("/component") + @Operation(summary = "Query Status Page Components") + public ResponseEntity<Message<List<ComponentStatus>>> queryStatusPageComponent() { + List<ComponentStatus> componentStatusList = statusPageService.queryComponentsStatus(); + 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); + return ResponseEntity.ok(Message.success(componentStatus)); + } + + @GetMapping("/incident") + @Operation(summary = "Query Status Page Incidents") + public ResponseEntity<Message<Page<StatusPageIncidentInfo>>> queryStatusPageIncident( + @Parameter(description = "Search-Target", example = "x") @RequestParam(required = false) String search, + @Parameter(description = "Start Time", example = "1756384301907") @RequestParam(required = false) Long startTime, + @Parameter(description = "End Time", example = "1756384301907") @RequestParam(required = false) Long endTime, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pages", example = "10") @RequestParam(defaultValue = "10") int pageSize) { + Page<StatusPageIncidentInfo> incidents = statusPageService.queryStatusPageIncidents(search, startTime, endTime, pageIndex, pageSize); + return ResponseEntity.ok(Message.success(incidents)); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/SummaryController.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/SummaryController.java new file mode 100644 index 0000000..0d8ea3b --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/SummaryController.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.manager.pojo.dto.AppCount; +import org.apache.hertzbeat.manager.pojo.dto.Dashboard; +import org.apache.hertzbeat.manager.service.MonitorService; +import org.springframework.beans.factory.annotation.Autowired; +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; + +/** + * System Summary Statistics API + */ +@Tag(name = "Summary Statistics API") +@RestController +@RequestMapping(path = "/api/summary", produces = {APPLICATION_JSON_VALUE}) +public class SummaryController { + + @Autowired + private MonitorService monitorService; + + @GetMapping + @Operation(summary = "Query all application category monitoring statistics", description = "Query all application category monitoring statistics") + public ResponseEntity<Message<Dashboard>> appMonitors() { + List<AppCount> appsCount = monitorService.getAllAppMonitorsCount(); + Message<Dashboard> message = Message.success(new Dashboard(appsCount)); + return ResponseEntity.ok(message); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/AuthTokenDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/AuthTokenDao.java new file mode 100644 index 0000000..d185cda --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/AuthTokenDao.java @@ -0,0 +1,58 @@ +/* + * 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.manager.dao; + +import org.apache.hertzbeat.common.entity.manager.AuthToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * AuthToken DAO + */ +public interface AuthTokenDao extends JpaRepository<AuthToken, Long>, JpaSpecificationExecutor<AuthToken> { + + /** + * Find all active tokens (status = 0) + */ + List<AuthToken> findByStatus(Byte status); + + /** + * Find active tokens created by the given user + */ + List<AuthToken> findByStatusAndCreator(Byte status, String creator); + + /** + * Check if an active token exists with the given hash + */ + boolean existsByTokenHashAndStatus(String tokenHash, Byte status); + + /** + * Update last used time for a token identified by its hash + */ + @Modifying + @Transactional + @Query("UPDATE AuthToken t SET t.lastUsedTime = :lastUsedTime WHERE t.tokenHash = :tokenHash") + void updateLastUsedTime(@Param("tokenHash") String tokenHash, @Param("lastUsedTime") LocalDateTime lastUsedTime); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/BulletinDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/BulletinDao.java new file mode 100644 index 0000000..7ad5486 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/BulletinDao.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.dao; + + +import java.util.List; +import org.apache.hertzbeat.common.entity.manager.Bulletin; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * BulletinDao + */ +public interface BulletinDao extends JpaRepository<Bulletin, Long>, JpaSpecificationExecutor<Bulletin> { + /** + * Delete Bulletin by name + */ + void deleteByNameIn(List<String> names); + + /** + * Get Bulletin by name + */ + Bulletin findByName(String name); + + /** + * Count Bulletin by name + */ + int countByName(String name); + + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/CollectorDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/CollectorDao.java new file mode 100644 index 0000000..8affacd --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/CollectorDao.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.dao; + +import java.util.List; +import java.util.Optional; +import org.apache.hertzbeat.common.entity.manager.Collector; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; + +/** + * Collector repository + */ +public interface CollectorDao extends JpaRepository<Collector, Long>, JpaSpecificationExecutor<Collector> { + + /** + * find collector by name + * @param name name + * @return collector + */ + Optional<Collector> findCollectorByName(String name); + + /** + * find collectors by names + * @param names collector name list + * @return collector list + */ + List<Collector> findCollectorsByNameIn(List<String> names); + + /** + * delete collector by name + * @param collector collector name + */ + @Modifying + void deleteCollectorByName(String collector); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/CollectorMonitorBindDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/CollectorMonitorBindDao.java new file mode 100644 index 0000000..d830f2e --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/CollectorMonitorBindDao.java @@ -0,0 +1,61 @@ +/* + * 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.manager.dao; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.apache.hertzbeat.common.entity.manager.CollectorMonitorBind; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; + +/** + * CollectorMonitorBind repository + */ +public interface CollectorMonitorBindDao extends JpaRepository<CollectorMonitorBind, Long>, JpaSpecificationExecutor<CollectorMonitorBind> { + + /** + * find monitors by collector + * @param collector collector + * @return monitor bind + */ + List<CollectorMonitorBind> findCollectorMonitorBindsByCollector(String collector); + + /** + * find monitor collector bind by monitor ids + * @param monitorIds monitor ids + * @return binds + */ + List<CollectorMonitorBind> findCollectorMonitorBindsByMonitorIdIn(Set<Long> monitorIds); + + /** + * find bind collector by monitor id + * @param monitorId monitor id + * @return collector bind + */ + Optional<CollectorMonitorBind> findCollectorMonitorBindByMonitorId(Long monitorId); + + + /** + * delete bind by monitor id + * @param monitorId monitor id + */ + @Modifying + void deleteCollectorMonitorBindsByMonitorId(Long monitorId); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/DefineDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/DefineDao.java new file mode 100644 index 0000000..cd04776 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/DefineDao.java @@ -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.manager.dao; + +import org.apache.hertzbeat.common.entity.manager.Define; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * monitor define repository + */ +public interface DefineDao extends JpaRepository<Define, String>, JpaSpecificationExecutor<Define> { +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/MetricsFavoriteDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/MetricsFavoriteDao.java new file mode 100644 index 0000000..a183133 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/MetricsFavoriteDao.java @@ -0,0 +1,75 @@ +/* + * 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.manager.dao; + +import org.apache.hertzbeat.common.entity.manager.MetricsFavorite; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * MetricsFavorite dao + */ +public interface MetricsFavoriteDao extends JpaRepository<MetricsFavorite, Long> { + + /** + * Find metrics favorite by creator and monitor id and metrics name + * + * @param creator user id + * @param monitorId monitor id + * @param metricsName metrics name + * @return optional metrics favorite + */ + Optional<MetricsFavorite> findByCreatorAndMonitorIdAndMetricsName(String creator, Long monitorId, String metricsName); + + /** + * Find all metrics favorites by user id and monitor id + * + * @param creator user id + * @param monitorId monitor id + * @return list of metrics favorites + */ + List<MetricsFavorite> findByCreatorAndMonitorId(String creator, Long monitorId); + + /** + * Delete metrics favorite by user id and monitor id and metrics name + * + * @param creator user id + * @param monitorId monitor id + * @param metricsName metrics name + */ + @Modifying + @Query("DELETE FROM MetricsFavorite mf WHERE mf.creator = :creator AND mf.monitorId = :monitorId AND mf.metricsName = :metricsName") + void deleteByUserIdAndMonitorIdAndMetricsName(@Param("creator") String creator, + @Param("monitorId") Long monitorId, + @Param("metricsName") String metricsName); + + /** + * Delete metrics favorites by monitor ids + * + * @param monitorIds monitor ids + */ + @Modifying + @Query("DELETE FROM MetricsFavorite mf WHERE mf.monitorId IN :monitorIds") + void deleteFavoritesByMonitorIdIn(@Param("monitorIds") Set<Long> monitorIds); +} \ No newline at end of file diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/MonitorBindDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/MonitorBindDao.java new file mode 100644 index 0000000..c9bc73c --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/MonitorBindDao.java @@ -0,0 +1,47 @@ +/* + * 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.manager.dao; + +import java.util.List; +import java.util.Set; + +import org.apache.hertzbeat.common.entity.manager.MonitorBind; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.transaction.annotation.Transactional; + +/** + * MonitorBind database operation + */ +public interface MonitorBindDao extends JpaRepository<MonitorBind, Long>, JpaSpecificationExecutor<MonitorBind> { + + List<MonitorBind> findMonitorBindsByBizId(Long bizId); + + List<MonitorBind> findMonitorBindsByBizIdIn(Set<Long> bizIds); + + + void deleteByMonitorId(Long monitorId); + + @Modifying + @Transactional + void deleteMonitorBindByBizIdAndMonitorId(Long bizId, Long monitorId); + + @Modifying + void deleteMonitorBindByBizIdIn(Set<Long> bizIds); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/MonitorDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/MonitorDao.java new file mode 100644 index 0000000..4a4a44a --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/MonitorDao.java @@ -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.manager.dao; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.manager.pojo.dto.AppCount; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * AuthResources database operation + */ +public interface MonitorDao extends JpaRepository<Monitor, Long>, JpaSpecificationExecutor<Monitor> { + + /** + * Delete monitor based on monitor ID list + * @param monitorIds Monitoring ID List + */ + void deleteAllByIdIn(Set<Long> monitorIds); + + /** + * Query monitoring based on monitoring ID list + * @param monitorIds Monitoring ID List + * @return Monitor List + */ + List<Monitor> findMonitorsByIdIn(Set<Long> monitorIds); + + /** + * Query monitoring by monitoring type + * @param app Monitor Type + * @return Monitor List + */ + List<Monitor> findMonitorsByAppEquals(String app); + + /** + * Querying Monitoring of Sent Collection Tasks + * @param status Monitor Status + * @return Monitor List + */ + List<Monitor> findMonitorsByStatusNotInAndJobIdNotNull(List<Byte> status); + + /** + * Query monitoring by monitoring name + * @param name monitoring name + * @return monitoring list + */ + Optional<Monitor> findMonitorByNameEquals(String name); + + /** + * Query the monitoring category - the number of monitoring corresponding to the status + * @return Monitoring Category-Status and Monitoring Quantity Mapping + */ + @Query("select new org.apache.hertzbeat.manager.pojo.dto.AppCount(mo.app, mo.status, COUNT(mo.id)) from Monitor mo group by mo.app, mo.status") + List<AppCount> findAppsStatusCount(); + + /** + * Update the status of the specified monitor + * @param id Monitor ID + * @param status Monitor Status + */ + @Modifying(clearAutomatically = true) + @Query("update Monitor set status = :status where id = :id") + void updateMonitorStatus(@Param(value = "id") Long id, @Param(value = "status") byte status); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ParamDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ParamDao.java new file mode 100644 index 0000000..3b31954 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ParamDao.java @@ -0,0 +1,48 @@ +/* + * 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.manager.dao; + +import java.util.List; +import java.util.Set; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * ParamDao database operations + */ +public interface ParamDao extends JpaRepository<Param, Long> { + + /** + * Query the list of parameters associated with the monitoring ID' + * @param monitorId Monitor ID + * @return list of parameter values + */ + List<Param> findParamsByMonitorId(Long monitorId); + + /** + * Remove the parameter list associated with the monitoring ID based on it + * @param monitorId Monitor Id + */ + void deleteParamsByMonitorId(long monitorId); + + /** + * Remove the parameter list associated with the monitoring ID list based on it + * @param monitorIds Monitoring ID List + */ + void deleteParamsByMonitorIdIn(Set<Long> monitorIds); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ParamDefineDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ParamDefineDao.java new file mode 100644 index 0000000..c3a6e5d --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/ParamDefineDao.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.dao; + +import java.util.List; +import org.apache.hertzbeat.common.entity.manager.ParamDefine; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * Param Define Database Operations + */ +public interface ParamDefineDao extends JpaRepository<ParamDefine, Long> { + + /** + * Query the parameter definitions under it according to the monitoring type + * @param app Monitoring type + * @return parameter definition list + */ + List<ParamDefine> findParamDefinesByApp(String app); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/PluginItemDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/PluginItemDao.java new file mode 100644 index 0000000..d77ec2b --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/PluginItemDao.java @@ -0,0 +1,31 @@ +/* + * 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.manager.dao; + +import java.util.List; +import org.apache.hertzbeat.common.entity.manager.PluginItem; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * plugin metadata repository + */ +public interface PluginItemDao extends JpaRepository<PluginItem, Long>, JpaSpecificationExecutor<PluginItem> { + + int countPluginItemByClassIdentifierIn(List<String> classIdentifiers); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/PluginMetadataDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/PluginMetadataDao.java new file mode 100644 index 0000000..bf24081 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/PluginMetadataDao.java @@ -0,0 +1,44 @@ +/* + * 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.manager.dao; + +import java.util.List; +import org.apache.hertzbeat.common.entity.manager.PluginMetadata; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * plugin metadata repository + */ +public interface PluginMetadataDao extends JpaRepository<PluginMetadata, Long>, JpaSpecificationExecutor<PluginMetadata> { + + /** + * count by name + * + * @param name name + * @return count + */ + int countPluginMetadataByName(String name); + + + /** + * find enabled plugins + * @return plugins + */ + List<PluginMetadata> findPluginMetadataByEnableStatusTrue(); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/PluginParamDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/PluginParamDao.java new file mode 100644 index 0000000..4eeec26 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/PluginParamDao.java @@ -0,0 +1,48 @@ +/* + * 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.manager.dao; + +import java.util.List; +import java.util.Set; +import org.apache.hertzbeat.manager.pojo.dto.PluginParam; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * PluginParamDao database operations + */ +public interface PluginParamDao extends JpaRepository<PluginParam, Long> { + + /** + * Query the list of parameters associated with the monitoring ID' + * @param pluginMetadataId Monitor ID + * @return list of parameter values + */ + List<PluginParam> findParamsByPluginMetadataId(Long pluginMetadataId); + + /** + * Remove the parameter list associated with the pluginMetadata ID based on it + * @param pluginMetadataId Monitor Id + */ + void deletePluginParamsByPluginMetadataId(long pluginMetadataId); + + /** + * Remove the parameter list associated with the pluginMetadata ID list based on it + * @param pluginMetadataIds Monitoring ID List + */ + void deletePluginParamsByPluginMetadataIdIn(Set<Long> pluginMetadataIds); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageComponentDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageComponentDao.java new file mode 100644 index 0000000..74d6428 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageComponentDao.java @@ -0,0 +1,36 @@ +/* + * 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.manager.dao; + +import java.util.List; +import org.apache.hertzbeat.common.entity.manager.StatusPageComponent; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * StatusPageComponent DAO interface. + */ +public interface StatusPageComponentDao extends JpaRepository<StatusPageComponent, Long>, JpaSpecificationExecutor<StatusPageComponent> { + + /** + * find by org id. + * @param orgId org id + * @return StatusPageComponent list + */ + List<StatusPageComponent> findByOrgId(long orgId); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageHistoryDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageHistoryDao.java new file mode 100644 index 0000000..beda8d3 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageHistoryDao.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.dao; + +import java.util.List; +import org.apache.hertzbeat.common.entity.manager.StatusPageHistory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * StatusPageOrg DAO interface. + */ +public interface StatusPageHistoryDao extends JpaRepository<StatusPageHistory, Long>, JpaSpecificationExecutor<StatusPageHistory> { + + /** + * find status page history by timestamp between start and end. + * @param start start timestamp + * @param end end timestamp + * @return status page history list + */ + List<StatusPageHistory> findStatusPageHistoriesByTimestampBetween(long start, long end); + + /** + * find status page history by component id and timestamp between start and end. + * @param componentId component id + * @param start start timestamp + * @param end end timestamp + * @return status page history list + */ + List<StatusPageHistory> findStatusPageHistoriesByComponentIdAndTimestampBetween(long componentId, long start, long end); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageIncidentComponentBindDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageIncidentComponentBindDao.java new file mode 100644 index 0000000..615ccbc --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageIncidentComponentBindDao.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.dao; + +import org.apache.hertzbeat.common.entity.manager.StatusPageIncidentComponentBind; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * StatusPageComponent DAO interface. + */ +public interface StatusPageIncidentComponentBindDao extends JpaRepository<StatusPageIncidentComponentBind, Long>, JpaSpecificationExecutor<StatusPageIncidentComponentBind> { + + /** + * count by component id + * @param componentId component id + * @return count + */ + long countByComponentId(long componentId); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageIncidentDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageIncidentDao.java new file mode 100644 index 0000000..c213a36 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageIncidentDao.java @@ -0,0 +1,29 @@ +/* + * 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.manager.dao; + +import org.apache.hertzbeat.common.entity.manager.StatusPageIncident; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * StatusPageIncident DAO interface. + */ +public interface StatusPageIncidentDao extends JpaRepository<StatusPageIncident, Long>, JpaSpecificationExecutor<StatusPageIncident> { + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageOrgDao.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageOrgDao.java new file mode 100644 index 0000000..eb5ed9c --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/StatusPageOrgDao.java @@ -0,0 +1,29 @@ +/* + * 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.manager.dao; + +import org.apache.hertzbeat.common.entity.manager.StatusPageOrg; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * StatusPageOrg DAO interface. + */ +public interface StatusPageOrgDao extends JpaRepository<StatusPageOrg, Long>, JpaSpecificationExecutor<StatusPageOrg> { + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/nativex/HertzbeatRuntimeHintsRegistrar.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/nativex/HertzbeatRuntimeHintsRegistrar.java new file mode 100644 index 0000000..44d9b3a --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/nativex/HertzbeatRuntimeHintsRegistrar.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.manager.nativex; + +import java.lang.reflect.Constructor; +import java.util.Set; +import org.apache.sshd.common.channel.ChannelListener; +import org.apache.sshd.common.forward.PortForwardingEventListener; +import org.apache.sshd.common.io.nio2.Nio2ServiceFactory; +import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory; +import org.apache.sshd.common.session.SessionListener; +import org.apache.sshd.common.util.security.bouncycastle.BouncyCastleSecurityProviderRegistrar; +import org.apache.sshd.common.util.security.eddsa.EdDSASecurityProviderRegistrar; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.lang.NonNull; +import org.springframework.util.ClassUtils; + +/** + * Derived from SpringCloud org.springframework.cloud.config.server.config.ConfigServerRuntimeHints + * @see <a href="https://github.com/spring-cloud/spring-cloud-config/blob/main/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerRuntimeHints.java">ConfigServerRuntimeHints</a> + */ +public class HertzbeatRuntimeHintsRegistrar implements RuntimeHintsRegistrar { + + private static final String SshConstantsClassName = "org.apache.sshd.common.SshConstants"; + + @Override + public void registerHints(@NonNull RuntimeHints hints, ClassLoader classLoader) { + // see: https://github.com/spring-cloud/spring-cloud-config/blob/main/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerRuntimeHints.java + // TODO: move over to GraalVM reachability metadata + if (ClassUtils.isPresent(SshConstantsClassName, classLoader)) { + hints.reflection().registerTypes(Set.of(TypeReference.of(BouncyCastleSecurityProviderRegistrar.class), + TypeReference.of(EdDSASecurityProviderRegistrar.class), TypeReference.of(Nio2ServiceFactory.class), + TypeReference.of(Nio2ServiceFactoryFactory.class)), + hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + hints.reflection().registerTypes(Set.of(TypeReference.of(PortForwardingEventListener.class)), + hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.DECLARED_FIELDS)); + hints.proxies().registerJdkProxy(TypeReference.of(ChannelListener.class), + TypeReference.of(PortForwardingEventListener.class), TypeReference.of(SessionListener.class)); + } + } + + private void registerConstructor(RuntimeHints hints, Class<?> clazz) { + Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors(); + for (Constructor<?> declaredConstructor : declaredConstructors) { + hints.reflection().registerConstructor(declaredConstructor, ExecutableMode.INVOKE); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AiControllerRequestParam.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AiControllerRequestParam.java new file mode 100644 index 0000000..5a94031 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AiControllerRequestParam.java @@ -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. + */ + +package org.apache.hertzbeat.manager.pojo.dto; + +import lombok.Data; + +/** + * AiControllerRequestParam + */ +@Data +public class AiControllerRequestParam { + + /** + * required parameter + */ + String text; +} \ No newline at end of file diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AiMessage.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AiMessage.java new file mode 100644 index 0000000..61001a8 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AiMessage.java @@ -0,0 +1,41 @@ +/* + * 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.manager.pojo.dto; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * ai message + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AiMessage { + /** + * role + */ + private String role; + + /** + * content + */ + private String content; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AliAiRequestParamDTO.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AliAiRequestParamDTO.java new file mode 100644 index 0000000..8d30624 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AliAiRequestParamDTO.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.pojo.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * Alibaba Ai Request param + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AliAiRequestParamDTO { + + /** + * ai version + */ + private String model; + + /** + * Enter information about the model + */ + private Input input; + + /** + * Parameters used to control model generation + */ + private Parameters parameters; + + /** + * Input + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + public static class Input { + + /** + * request message + */ + private List<AiMessage> messages; + + } + + /** + * Parameters + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder + public static class Parameters { + + /** + * The model outputs the maximum tokens, with a maximum output of 8192 and a default value of 1024 + */ + @JsonProperty("max_tokens") + private Integer maxTokens; + + /** + * Used to control the degree of randomness and variety. Specifically, the temperature value controls the + * degree to which the probability distribution for each candidate word is smoothed when text is generated. + * A higher temperature will reduce the peak value of the probability distribution, so that more low-probability + * words will be selected and the results will be more diversified. A lower temperature will increase the peak of the probability distribution, + * making it easier for high-probability words to be selected and producing more certain results. + */ + private float temperature; + + /** + * The Internet search service is built into the model, and this parameter controls whether the model refers + * to the Internet search results when generating text. The value can be: + * true: If Internet search is enabled, the model uses the search results as reference information in the text + * generation process, but the model "decides" whether to use the Internet search results based on its internal logic. + * false: Turn off Internet search. + */ + @JsonProperty("enable_search") + private boolean enableSearch; + + /** + * Set return format,default message + */ + @JsonProperty("result_format") + private String resultFormat; + + /** + * Control whether incremental output is enabled in stream output mode, that is, whether the subsequent output content contains + * the output content. If the value is set to True, the incremental output mode will be enabled, and the subsequent output will + * not contain the output content, you need to splice the overall output by yourself. Set to False to contain the output. + */ + @JsonProperty("incremental_output") + private boolean incrementalOutput; + + } + + + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AliAiResponse.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AliAiResponse.java new file mode 100644 index 0000000..e2562cc --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AliAiResponse.java @@ -0,0 +1,112 @@ +/* + * 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.manager.pojo.dto; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * AliAiResponse + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AliAiResponse { + + /** + * response + */ + private AliAiOutput output; + + /** + * Returns the number of tokens invoked by the model at the end. + */ + private Tokens usage; + + /** + * AliAiOutput + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class AliAiOutput { + + /** + * response message + */ + private List<Choice> choices; + + } + + /** + * Choice + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Choice { + + /** + * Stop cause: + * null: being generated + * stop: stop token causes the end + * length: indicates that the generation length ends + */ + @JsonProperty("finish_reason") + private String finishReason; + + /** + * response message + */ + private AiMessage message; + } + + /** + * Tokens + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public class Tokens { + + /** + * The number of tokens of the model output content + */ + @JsonProperty("output_tokens") + private Integer outputTokens; + + /** + * The number of tokens entered this request. + * When enable_search is set to true, the number of tokens entered is greater than the number of + * tokens you entered the request because you need to add search related content. + */ + @JsonProperty("input_tokens") + private Integer inputTokens; + + /** + * usage.output_tokens and usage.input_tokens sum + */ + @JsonProperty("total_tokens") + private Integer totalTokens; + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AppCount.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AppCount.java new file mode 100644 index 0000000..4cd67c4 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/AppCount.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.pojo.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * AppCount class + */ +@AllArgsConstructor +@NoArgsConstructor +@Data +public class AppCount { + + public AppCount(String app, byte status, Long size) { + this.app = app; + this.status = status; + this.size = size; + } + + /** + * monitor category + */ + private String category; + + /** + * monitor type + */ + private String app; + + /** + * task status + */ + private transient byte status; + + /** + * monitor count + */ + private long size; + + /** + * number of available tasks + */ + private long availableSize; + + /** + * number of tasks with unmonitored status + */ + private long unManageSize; + + /** + * number of tasks with unavailable status + */ + private long unAvailableSize; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/BulletinMetricsData.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/BulletinMetricsData.java new file mode 100644 index 0000000..0a288cd --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/BulletinMetricsData.java @@ -0,0 +1,135 @@ +/* + * 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.manager.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Bulletin Metrics Data + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Bulletin Metrics Data") +public class BulletinMetricsData { + + /** + * Bulletin Name + */ + @Schema(title = "Bulletin Name") + private String name; + + /** + * Content Data + */ + @Schema(description = "Content Data") + private List<Data> content; + + /** + * Bulletin Metrics Data + */ + @lombok.Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class Data { + + /** + * Monitor Name + */ + @Schema(title = "Monitor name") + private String monitorName; + + /** + * Monitor ID + */ + @Schema(title = "Monitor ID") + private Long monitorId; + + /** + * Monitor IP + */ + @Schema(title = "Monitor IP") + private String host; + + /** + * Monitor Metrics + */ + @Schema(title = "Monitor Metrics") + private List<Metric> metrics; + } + + /** + * Metrics Data + */ + @lombok.Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + @Schema(description = "Metrics Data") + public static class Metric{ + + /** + * Metric type + */ + @Schema(title = "Metric type") + private String name; + + /** + * Metric fields + */ + @Schema(title = "Metric fields") + private List<List<Field>> fields; + } + + + /** + * Metrics field + */ + @lombok.Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + @Schema(description = "Metrics field") + public static class Field{ + + /** + * Field name + */ + @Schema(title = "Field name") + private String key; + + /** + * Field unit + */ + @Schema(title = "Field unit") + private String unit; + + /** + * Field value + */ + @Schema(title = "Field value") + private String value; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/CollectorInfo.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/CollectorInfo.java new file mode 100644 index 0000000..e8366b9 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/CollectorInfo.java @@ -0,0 +1,82 @@ +/* + * 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.manager.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.Collector; + +/** + * Collector info view. + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "collector info") +public class CollectorInfo { + + @Schema(title = "primary id", example = "2") + private Long id; + + @Schema(title = "collector identity name", description = "collector identity name") + private String name; + + @Schema(title = "collector ip", description = "collector remote ip") + private String ip; + + @Schema(title = "collector version", description = "collector version") + private String version; + + @Schema(title = "collector status: 0-online 1-offline") + private byte status; + + @Schema(title = "collector mode: public or private") + private String mode; + + @Schema(title = "The creator of this record", example = "tom") + private String creator; + + @Schema(title = "This record was last modified by") + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)") + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)") + private LocalDateTime gmtUpdate; + + public static CollectorInfo fromEntity(Collector collector) { + return CollectorInfo.builder() + .id(collector.getId()) + .name(collector.getName()) + .ip(collector.getIp()) + .version(collector.getVersion()) + .status(collector.getStatus()) + .mode(collector.getMode()) + .creator(collector.getCreator()) + .modifier(collector.getModifier()) + .gmtCreate(collector.getGmtCreate()) + .gmtUpdate(collector.getGmtUpdate()) + .build(); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/CollectorSummary.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/CollectorSummary.java new file mode 100644 index 0000000..1862ff2 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/CollectorSummary.java @@ -0,0 +1,44 @@ +/* + * 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.manager.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Collector summary view. + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "collector summary") +public class CollectorSummary { + + @Schema(description = "the collector info") + private CollectorInfo collector; + + @Schema(description = "the number of monitors pinned in this collector") + private int pinMonitorNum; + + @Schema(description = "the number of monitors dispatched in this collector") + private int dispatchMonitorNum; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ComponentStatus.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ComponentStatus.java new file mode 100644 index 0000000..af8f633 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ComponentStatus.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.pojo.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import org.apache.hertzbeat.common.entity.manager.StatusPageComponent; +import org.apache.hertzbeat.common.entity.manager.StatusPageHistory; +import java.util.Objects; + +/** + * status page's component status dto + */ +@Schema(description = "Status Page's Component Status") +public class ComponentStatus { + + private StatusPageComponentInfo componentInfo; + + private List<StatusPageHistoryInfo> historyItems; + + @Schema(description = "Component Info") + @JsonProperty("info") + public StatusPageComponentInfo getComponentInfo() { + return componentInfo; + } + + @JsonProperty("info") + public void setComponentInfo(StatusPageComponentInfo componentInfo) { + this.componentInfo = componentInfo; + } + + @Schema(description = "Component History") + @JsonProperty("history") + public List<StatusPageHistoryInfo> getHistoryItems() { + return historyItems; + } + + @JsonProperty("history") + public void setHistoryItems(List<StatusPageHistoryInfo> historyItems) { + this.historyItems = historyItems; + } + + @JsonIgnore + public StatusPageComponent getInfo() { + return componentInfo == null ? null : componentInfo.toEntity(); + } + + public void setInfo(StatusPageComponent info) { + this.componentInfo = StatusPageComponentInfo.fromEntity(info); + } + + @JsonIgnore + public List<StatusPageHistory> getHistory() { + return historyItems == null ? null : historyItems.stream() + .filter(Objects::nonNull) + .map(StatusPageHistoryInfo::toEntity) + .toList(); + } + + public void setHistory(List<StatusPageHistory> history) { + this.historyItems = history == null ? null : history.stream() + .filter(Objects::nonNull) + .map(StatusPageHistoryInfo::fromEntity) + .toList(); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/Dashboard.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/Dashboard.java new file mode 100644 index 0000000..6af1861 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/Dashboard.java @@ -0,0 +1,37 @@ +/* + * 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.manager.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Large screen dashboard statistics + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Dashboard App Count Info") +public class Dashboard { + + List<AppCount> apps; + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/FileDTO.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/FileDTO.java new file mode 100644 index 0000000..fa93a81 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/FileDTO.java @@ -0,0 +1,36 @@ +/* + * 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.manager.pojo.dto; + +import java.io.InputStream; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * File storage + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileDTO { + private String name; + private InputStream inputStream; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/Hierarchy.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/Hierarchy.java new file mode 100644 index 0000000..a56877e --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/Hierarchy.java @@ -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.manager.pojo.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Hierarchical structure + * eg: Monitoring Type metrics Information Hierarchy Relationship + */ +@AllArgsConstructor +@NoArgsConstructor +@Data +@Schema(description = "Monitor Hierarchy") +public class Hierarchy { + + /** + * Category value + */ + @Schema(description = "Category Value", example = "os", accessMode = READ_WRITE) + String category; + + /** + * Attribute value + */ + @Schema(description = "Attribute value", example = "linux", accessMode = READ_WRITE) + String value; + + /** + * Attribute internationalization tag + */ + @Schema(description = "Attribute internationalization tag", example = "Linux system", accessMode = READ_WRITE) + String label; + + /** + * Is it a leaf node + */ + @Schema(description = "Is it a leaf node", example = "true", accessMode = READ_WRITE) + Boolean isLeaf = false; + + /** + * Is hide this app type in main menus layout + */ + @Schema(description = "Is hide this app in main menus layout, only for app type, default true.", example = "true") + Boolean hide = true; + + /** + * For leaf metric + * metric type 0-number: number 1-string: string + */ + @Schema(description = "metric type 0-number: number 1-string: string") + private Byte type; + + /** + * metric unit + */ + @Schema(description = "metric unit") + private String unit; + + /** + * Next level of association + */ + @Schema(description = "Next Hierarchy", accessMode = READ_WRITE) + private List<Hierarchy> children; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/LoginDto.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/LoginDto.java new file mode 100644 index 0000000..29738c3 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/LoginDto.java @@ -0,0 +1,63 @@ +/* + * 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.manager.pojo.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Login registered account information transfer body username phone email + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Account information transfer body") +public class LoginDto { + + /** + * type + * 1. Account (email username and mobile phone number) password login 2. github login 3. WeChat login + */ + @Schema(description = "type", example = "1", accessMode = READ_ONLY) + @Max(value = 4, message = "1. Account (email username and mobile phone number) password login 2. github login 3. WeChat login") + private Byte type; + + /** + * User ID + */ + @Schema(description = "user identification", example = "1", accessMode = READ_ONLY) + @NotBlank(message = "Identifier can not null") + private String identifier; + + /** + * key + */ + @Schema(description = "Secret key", example = "1", accessMode = READ_ONLY) + @NotBlank(message = "Credential can not null") + @Size(max = 512, message = "credential max length 512") + private String credential; + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MetricsInfo.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MetricsInfo.java new file mode 100644 index 0000000..217ab54 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MetricsInfo.java @@ -0,0 +1,41 @@ +/* + * 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.manager.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Metrics Information with favorite status + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Metrics information with favorite status") +public class MetricsInfo { + + @Schema(description = "Metrics name", example = "cpu") + private String name; + + @Schema(description = "Whether the metrics is favorited by current user") + private Boolean favorited; +} \ No newline at end of file diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDefineDto.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDefineDto.java new file mode 100644 index 0000000..0bba87d --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDefineDto.java @@ -0,0 +1,42 @@ +/* + * 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.manager.pojo.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * monitor define + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Monitor Define transfer body") +public class MonitorDefineDto { + + @Schema(description = "Define content", example = "1", accessMode = READ_ONLY) + @NotBlank(message = "define can not null") + private String define; + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDto.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDto.java new file mode 100644 index 0000000..33f77bd --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDto.java @@ -0,0 +1,126 @@ +/* + * 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.manager.pojo.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Param; +import java.util.Objects; + +/** + * Monitoring Information External Interaction Entities + */ +@Schema(description = "Monitoring information entities") +public class MonitorDto { + + @NotNull + @Valid + private MonitorInfo monitorInfo; + + @NotEmpty + @Valid + private List<MonitorParam> paramInfos; + + private List<MetricsInfo> metrics; + + private String collector; + + private GrafanaDashboard grafanaDashboard; + + @Schema(description = "monitor content", accessMode = READ_WRITE) + @JsonProperty("monitor") + public MonitorInfo getMonitorInfo() { + return monitorInfo; + } + + @JsonProperty("monitor") + public void setMonitorInfo(MonitorInfo monitorInfo) { + this.monitorInfo = monitorInfo; + } + + @Schema(description = "monitor params", accessMode = READ_WRITE) + @JsonProperty("params") + public List<MonitorParam> getParamInfos() { + return paramInfos; + } + + @JsonProperty("params") + public void setParamInfos(List<MonitorParam> paramInfos) { + this.paramInfos = paramInfos; + } + + @JsonIgnore + public Monitor getMonitor() { + return monitorInfo == null ? null : monitorInfo.toEntity(); + } + + public void setMonitor(Monitor monitor) { + this.monitorInfo = MonitorInfo.fromEntity(monitor); + } + + @JsonIgnore + public List<Param> getParams() { + return paramInfos == null ? null : paramInfos.stream() + .filter(Objects::nonNull) + .map(MonitorParam::toEntity) + .toList(); + } + + public void setParams(List<Param> params) { + this.paramInfos = params == null ? null : params.stream() + .filter(Objects::nonNull) + .map(MonitorParam::fromEntity) + .toList(); + } + + @Schema(description = "Monitor Metrics", accessMode = READ_ONLY) + public List<MetricsInfo> getMetrics() { + return metrics; + } + + public void setMetrics(List<MetricsInfo> metrics) { + this.metrics = metrics; + } + + @Schema(description = "pinned collector, default null if system dispatch", accessMode = READ_WRITE) + public String getCollector() { + return collector; + } + + public void setCollector(String collector) { + this.collector = collector; + } + + @Schema(description = "grafana dashboard") + public GrafanaDashboard getGrafanaDashboard() { + return grafanaDashboard; + } + + public void setGrafanaDashboard(GrafanaDashboard grafanaDashboard) { + this.grafanaDashboard = grafanaDashboard; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorInfo.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorInfo.java new file mode 100644 index 0000000..e620943 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorInfo.java @@ -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.manager.pojo.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.support.valid.HostValid; + +/** + * Manager-side monitor DTO detached from JPA annotations. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class MonitorInfo { + + private Long id; + + private Long jobId; + + @Size(max = 100) + private String name; + + @Size(max = 100) + private String app; + + @Size(max = 100) + private String scrape; + + @Size(max = 100) + @HostValid + private String instance; + + @Min(10) + private Integer intervals; + + @Size(max = 20) + private String scheduleType; + + @Size(max = 100) + private String cronExpression; + + @Min(0) + @Max(4) + private byte status; + + private byte type; + + private Map<String, String> labels; + + private Map<String, String> annotations; + + @Size(max = 255) + private String description; + + private String creator; + + private String modifier; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; + + public static MonitorInfo fromEntity(Monitor monitor) { + if (monitor == null) { + return null; + } + MonitorInfo info = new MonitorInfo(); + info.setId(monitor.getId()); + info.setJobId(monitor.getJobId()); + info.setName(monitor.getName()); + info.setApp(monitor.getApp()); + info.setScrape(monitor.getScrape()); + info.setInstance(monitor.getInstance()); + info.setIntervals(monitor.getIntervals()); + info.setScheduleType(monitor.getScheduleType()); + info.setCronExpression(monitor.getCronExpression()); + info.setStatus(monitor.getStatus()); + info.setType(monitor.getType()); + info.setLabels(monitor.getLabels()); + info.setAnnotations(monitor.getAnnotations()); + info.setDescription(monitor.getDescription()); + info.setCreator(monitor.getCreator()); + info.setModifier(monitor.getModifier()); + info.setGmtCreate(monitor.getGmtCreate()); + info.setGmtUpdate(monitor.getGmtUpdate()); + return info; + } + + public Monitor toEntity() { + Monitor monitor = new Monitor(); + monitor.setId(id); + monitor.setJobId(jobId); + monitor.setName(name); + monitor.setApp(app); + monitor.setScrape(scrape); + monitor.setInstance(instance); + monitor.setIntervals(intervals); + monitor.setScheduleType(scheduleType); + monitor.setCronExpression(cronExpression); + monitor.setStatus(status); + monitor.setType(type); + monitor.setLabels(labels); + monitor.setAnnotations(annotations); + monitor.setDescription(description); + monitor.setCreator(creator); + monitor.setModifier(modifier); + monitor.setGmtCreate(gmtCreate); + monitor.setGmtUpdate(gmtUpdate); + return monitor; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorParam.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorParam.java new file mode 100644 index 0000000..4fdde3f --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorParam.java @@ -0,0 +1,81 @@ +/* + * 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.manager.pojo.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.Param; + +/** + * Manager-side monitor param DTO detached from JPA annotations. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class MonitorParam { + + private Long id; + + private Long monitorId; + + @Size(max = 100) + @NotBlank(message = "field can not null") + private String field; + + @Size(max = 8126) + private String paramValue; + + @Min(0) + private byte type; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; + + public static MonitorParam fromEntity(Param param) { + if (param == null) { + return null; + } + MonitorParam monitorParam = new MonitorParam(); + monitorParam.setId(param.getId()); + monitorParam.setMonitorId(param.getMonitorId()); + monitorParam.setField(param.getField()); + monitorParam.setParamValue(param.getParamValue()); + monitorParam.setType(param.getType()); + monitorParam.setGmtCreate(param.getGmtCreate()); + monitorParam.setGmtUpdate(param.getGmtUpdate()); + return monitorParam; + } + + public Param toEntity() { + Param param = new Param(); + param.setId(id); + param.setMonitorId(monitorId); + param.setField(field); + param.setParamValue(paramValue); + param.setType(type); + param.setGmtCreate(gmtCreate); + param.setGmtUpdate(gmtUpdate); + return param; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MuteConfig.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MuteConfig.java new file mode 100644 index 0000000..808ebbc --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MuteConfig.java @@ -0,0 +1,39 @@ +/* + * 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.manager.pojo.dto; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Mute Configuration + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MuteConfig { + /** + * mute + */ + private boolean mute; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ObjectStoreConfigChangeEvent.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ObjectStoreConfigChangeEvent.java new file mode 100644 index 0000000..70e3ca5 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ObjectStoreConfigChangeEvent.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.pojo.dto; + +import java.util.UUID; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +/** + * Object store configuration change event. + */ +@Getter +public class ObjectStoreConfigChangeEvent extends ApplicationEvent { + private final ObjectStoreDTO<?> config; + + public ObjectStoreConfigChangeEvent(ObjectStoreDTO<?> config) { + super(UUID.randomUUID()); + this.config = config; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ObjectStoreDTO.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ObjectStoreDTO.java new file mode 100644 index 0000000..c07d2a8 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ObjectStoreDTO.java @@ -0,0 +1,81 @@ +/* + * 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.manager.pojo.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * file storage container + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ObjectStoreDTO<T> { + + /** + * file storage service type + */ + private Type type; + + /** + * Configuration item + */ + private T config; + + /** + * file storage service type + */ + public enum Type { + + /** + * local file + */ + FILE, + + /** + * local database + */ + DATABASE, + + /** + * <a href="https://support.huaweicloud.com/obs/index.html">Huawei Cloud OBS</a> + */ + OBS + } + + /** + * file storage configuration + */ + @Data + public static class ObsConfig { + private String accessKey; + private String secretKey; + private String bucketName; + private String endpoint; + + /** + * Save path + */ + private String savePath = "hertzbeat"; + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java new file mode 100644 index 0000000..0630cdf --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineDto.java @@ -0,0 +1,36 @@ +/* + * 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.manager.pojo.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Parameters define transfer entities + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ParamDefineDto { + + private String app; + + private List<ParamDefineInfo> param; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineInfo.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineInfo.java new file mode 100644 index 0000000..3b54b57 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/ParamDefineInfo.java @@ -0,0 +1,144 @@ +/* + * 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.manager.pojo.dto; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.job.RuntimeParamDefine; +import org.apache.hertzbeat.common.entity.manager.ParamDefine; + +/** + * Manager-side parameter definition DTO detached from JPA annotations. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ParamDefineInfo { + + private Long id; + + private String app; + + private Map<String, String> name; + + private String field; + + private String type; + + private boolean required; + + private String defaultValue; + + private String placeholder; + + private String range; + + private Short limit; + + private List<OptionInfo> options; + + private String keyAlias; + + private String valueAlias; + + private boolean hide; + + private String creator; + + private String modifier; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; + + private Map<String, List<Object>> depend; + + public static ParamDefineInfo fromEntity(ParamDefine paramDefine) { + if (paramDefine == null) { + return null; + } + ParamDefineInfo info = new ParamDefineInfo(); + info.setId(paramDefine.getId()); + info.setApp(paramDefine.getApp()); + info.setName(paramDefine.getName()); + info.setField(paramDefine.getField()); + info.setType(paramDefine.getType()); + info.setRequired(paramDefine.isRequired()); + info.setDefaultValue(paramDefine.getDefaultValue()); + info.setPlaceholder(paramDefine.getPlaceholder()); + info.setRange(paramDefine.getRange()); + info.setLimit(paramDefine.getLimit()); + info.setOptions(paramDefine.getOptions() == null ? null + : paramDefine.getOptions().stream().map(OptionInfo::fromEntity).toList()); + info.setKeyAlias(paramDefine.getKeyAlias()); + info.setValueAlias(paramDefine.getValueAlias()); + info.setHide(paramDefine.isHide()); + info.setCreator(paramDefine.getCreator()); + info.setModifier(paramDefine.getModifier()); + info.setGmtCreate(paramDefine.getGmtCreate()); + info.setGmtUpdate(paramDefine.getGmtUpdate()); + info.setDepend(paramDefine.getDepend()); + return info; + } + + public static ParamDefineInfo fromRuntime(RuntimeParamDefine runtimeParamDefine) { + if (runtimeParamDefine == null) { + return null; + } + ParamDefineInfo info = new ParamDefineInfo(); + info.setApp(runtimeParamDefine.getApp()); + info.setName(runtimeParamDefine.getName()); + info.setField(runtimeParamDefine.getField()); + info.setType(runtimeParamDefine.getType()); + info.setRequired(runtimeParamDefine.isRequired()); + info.setDefaultValue(runtimeParamDefine.getDefaultValue()); + info.setPlaceholder(runtimeParamDefine.getPlaceholder()); + info.setRange(runtimeParamDefine.getRange()); + info.setLimit(runtimeParamDefine.getLimit()); + info.setOptions(runtimeParamDefine.getOptions() == null ? null + : runtimeParamDefine.getOptions().stream().map(OptionInfo::fromRuntime).toList()); + info.setKeyAlias(runtimeParamDefine.getKeyAlias()); + info.setValueAlias(runtimeParamDefine.getValueAlias()); + info.setHide(runtimeParamDefine.isHide()); + info.setDepend(runtimeParamDefine.getDepend()); + return info; + } + + /** + * DTO version of parameter options. + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class OptionInfo { + private String label; + private String value; + + public static OptionInfo fromEntity(ParamDefine.Option option) { + return option == null ? null : new OptionInfo(option.getLabel(), option.getValue()); + } + + public static OptionInfo fromRuntime(RuntimeParamDefine.Option option) { + return option == null ? null : new OptionInfo(option.getLabel(), option.getValue()); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParam.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParam.java new file mode 100644 index 0000000..faae0fe --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParam.java @@ -0,0 +1,115 @@ +/* + * 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.manager.pojo.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * PluginParam + */ +@Entity +@Table( + name = "hzb_plugin_param", + indexes = {@Index(name = "idx_hzb_plugin_param_plugin_metadata_id", columnList = "plugin_metadata_id")}, + uniqueConstraints = @UniqueConstraint(name = "uk_hzb_plugin_param_metadata_field", columnNames = {"plugin_metadata_id", "field"}) +) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Parameter Entity") +@EntityListeners(AuditingEntityListener.class) +public class PluginParam { + + /** + * Parameter primary key index ID + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Parameter primary key index ID", example = "87584674384", accessMode = READ_ONLY) + private Long id; + /** + * Monitor ID + */ + @NotNull + @Column(name = "plugin_metadata_id") + @Schema(title = "Plugin task ID", example = "875846754543", accessMode = READ_WRITE) + private Long pluginMetadataId; + + /** + * Parameter Field Identifier + */ + @Schema(title = "Parameter identifier field", example = "port", accessMode = READ_WRITE) + @Size(max = 100) + @NotNull + @Column(name = "field") + private String field; + + /** + * Param Value + */ + @Schema(title = "parameter values", example = "8080", accessMode = READ_WRITE) + @Size(max = 8126) + @Column(length = 8126) + private String paramValue; + + /** + * Parameter type 0: number 1: string 2: encrypted string 3: json string mapped by map + */ + @Schema(title = "Parameter types 0: number 1: string 2: encrypted string 3:map mapped json string 4:arrays string", + accessMode = READ_WRITE) + @Min(0) + private byte type; + + /** + * Record create time + */ + @Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + /** + * Record the latest modification time + */ + @Schema(title = "Record modify time", example = "1612198444000", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java new file mode 100644 index 0000000..d0bd64b --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginParametersVO.java @@ -0,0 +1,42 @@ +/* + * 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.manager.pojo.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Popup rendering and parameter values + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PluginParametersVO { + + /** + * Stencil rendering + */ + private List<ParamDefineInfo> paramDefines; + + /** + * specific parameter + */ + private List<PluginParam> pluginParams; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginUpload.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginUpload.java new file mode 100644 index 0000000..e914cd1 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/PluginUpload.java @@ -0,0 +1,42 @@ +/* + * 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.manager.pojo.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +/** + * Manager-side plugin upload request. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PluginUpload { + + @NotNull + private MultipartFile jarFile; + + @NotNull(message = "Plugin name is required") + private String name; + + @NotNull(message = "Enable status is required") + private Boolean enableStatus; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/RefreshTokenResponse.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/RefreshTokenResponse.java new file mode 100644 index 0000000..724dc77 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/RefreshTokenResponse.java @@ -0,0 +1,40 @@ +/* + * 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.manager.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Refresh Token Response + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "flash token response") +public class RefreshTokenResponse { + @Schema(title = "Access Token") + private String token; + + @Schema(title = "Refresh Token") + private String refreshToken; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageComponentInfo.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageComponentInfo.java new file mode 100644 index 0000000..f8ef3b3 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageComponentInfo.java @@ -0,0 +1,95 @@ +/* + * 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.manager.pojo.dto; + +import java.time.LocalDateTime; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.StatusPageComponent; + +/** + * Public status-page component DTO. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class StatusPageComponentInfo { + + private Long id; + + private Long orgId; + + private String name; + + private String description; + + private Map<String, String> labels; + + private byte method; + + private byte configState; + + private byte state; + + private String creator; + + private String modifier; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; + + public static StatusPageComponentInfo fromEntity(StatusPageComponent component) { + if (component == null) { + return null; + } + StatusPageComponentInfo info = new StatusPageComponentInfo(); + info.setId(component.getId()); + info.setOrgId(component.getOrgId()); + info.setName(component.getName()); + info.setDescription(component.getDescription()); + info.setLabels(component.getLabels()); + info.setMethod(component.getMethod()); + info.setConfigState(component.getConfigState()); + info.setState(component.getState()); + info.setCreator(component.getCreator()); + info.setModifier(component.getModifier()); + info.setGmtCreate(component.getGmtCreate()); + info.setGmtUpdate(component.getGmtUpdate()); + return info; + } + + public StatusPageComponent toEntity() { + StatusPageComponent component = new StatusPageComponent(); + component.setId(id); + component.setOrgId(orgId); + component.setName(name); + component.setDescription(description); + component.setLabels(labels); + component.setMethod(method); + component.setConfigState(configState); + component.setState(state); + component.setCreator(creator); + component.setModifier(modifier); + component.setGmtCreate(gmtCreate); + component.setGmtUpdate(gmtUpdate); + return component; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageHistoryInfo.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageHistoryInfo.java new file mode 100644 index 0000000..2f0a256 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageHistoryInfo.java @@ -0,0 +1,94 @@ +/* + * 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.manager.pojo.dto; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.StatusPageHistory; + +/** + * Public status-page history DTO. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class StatusPageHistoryInfo { + + private Long id; + + private Long componentId; + + private byte state; + + private Long timestamp; + + private Double uptime; + + private Integer abnormal; + + private Integer unknowing; + + private Integer normal; + + private String creator; + + private String modifier; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; + + public static StatusPageHistoryInfo fromEntity(StatusPageHistory history) { + if (history == null) { + return null; + } + StatusPageHistoryInfo info = new StatusPageHistoryInfo(); + info.setId(history.getId()); + info.setComponentId(history.getComponentId()); + info.setState(history.getState()); + info.setTimestamp(history.getTimestamp()); + info.setUptime(history.getUptime()); + info.setAbnormal(history.getAbnormal()); + info.setUnknowing(history.getUnknowing()); + info.setNormal(history.getNormal()); + info.setCreator(history.getCreator()); + info.setModifier(history.getModifier()); + info.setGmtCreate(history.getGmtCreate()); + info.setGmtUpdate(history.getGmtUpdate()); + return info; + } + + public StatusPageHistory toEntity() { + StatusPageHistory history = new StatusPageHistory(); + history.setId(id); + history.setComponentId(componentId); + history.setState(state); + history.setTimestamp(timestamp); + history.setUptime(uptime); + history.setAbnormal(abnormal); + history.setUnknowing(unknowing); + history.setNormal(normal); + history.setCreator(creator); + history.setModifier(modifier); + history.setGmtCreate(gmtCreate); + history.setGmtUpdate(gmtUpdate); + return history; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageIncidentContentInfo.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageIncidentContentInfo.java new file mode 100644 index 0000000..3d27f5a --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageIncidentContentInfo.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.pojo.dto; + +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.StatusPageIncidentContent; + +/** + * Manager-side status-page incident content DTO detached from JPA annotations. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class StatusPageIncidentContentInfo { + + private Long id; + + private Long incidentId; + + @NotBlank + private String message; + + private byte state; + + private Long timestamp; + + private String creator; + + private String modifier; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; + + public static StatusPageIncidentContentInfo fromEntity(StatusPageIncidentContent content) { + if (content == null) { + return null; + } + StatusPageIncidentContentInfo info = new StatusPageIncidentContentInfo(); + info.setId(content.getId()); + info.setIncidentId(content.getIncidentId()); + info.setMessage(content.getMessage()); + info.setState(content.getState()); + info.setTimestamp(content.getTimestamp()); + info.setCreator(content.getCreator()); + info.setModifier(content.getModifier()); + info.setGmtCreate(content.getGmtCreate()); + info.setGmtUpdate(content.getGmtUpdate()); + return info; + } + + public StatusPageIncidentContent toEntity() { + StatusPageIncidentContent content = new StatusPageIncidentContent(); + content.setId(id); + content.setIncidentId(incidentId); + content.setMessage(message); + content.setState(state); + content.setTimestamp(timestamp); + content.setCreator(creator); + content.setModifier(modifier); + content.setGmtCreate(gmtCreate); + content.setGmtUpdate(gmtUpdate); + return content; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageIncidentInfo.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageIncidentInfo.java new file mode 100644 index 0000000..0bd53f6 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageIncidentInfo.java @@ -0,0 +1,115 @@ +/* + * 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.manager.pojo.dto; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.StatusPageIncident; + +/** + * Manager-side status-page incident DTO detached from JPA annotations. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class StatusPageIncidentInfo { + + private Long id; + + private Long orgId; + + @NotBlank + private String name; + + private byte state; + + private Long startTime; + + private Long endTime; + + private String creator; + + private String modifier; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; + + @Valid + private List<StatusPageComponentInfo> components; + + @Valid + private List<StatusPageIncidentContentInfo> contents; + + public static StatusPageIncidentInfo fromEntity(StatusPageIncident incident) { + if (incident == null) { + return null; + } + StatusPageIncidentInfo info = new StatusPageIncidentInfo(); + info.setId(incident.getId()); + info.setOrgId(incident.getOrgId()); + info.setName(incident.getName()); + info.setState(incident.getState()); + info.setStartTime(incident.getStartTime()); + info.setEndTime(incident.getEndTime()); + info.setCreator(incident.getCreator()); + info.setModifier(incident.getModifier()); + info.setGmtCreate(incident.getGmtCreate()); + info.setGmtUpdate(incident.getGmtUpdate()); + info.setComponents(incident.getComponents() == null ? null : incident.getComponents().stream() + .filter(Objects::nonNull) + .map(StatusPageComponentInfo::fromEntity) + .toList()); + info.setContents(incident.getContents() == null ? null : incident.getContents().stream() + .filter(Objects::nonNull) + .map(StatusPageIncidentContentInfo::fromEntity) + .toList()); + return info; + } + + public StatusPageIncident toEntity() { + StatusPageIncident incident = new StatusPageIncident(); + incident.setId(id); + incident.setOrgId(orgId); + incident.setName(name); + incident.setState(state); + incident.setStartTime(startTime); + incident.setEndTime(endTime); + incident.setCreator(creator); + incident.setModifier(modifier); + incident.setGmtCreate(gmtCreate); + incident.setGmtUpdate(gmtUpdate); + incident.setComponents(components == null ? null : components.stream() + .filter(Objects::nonNull) + .map(StatusPageComponentInfo::toEntity) + .toList()); + incident.setContents(contents == null ? null : contents.stream() + .filter(Objects::nonNull) + .map(StatusPageIncidentContentInfo::toEntity) + .collect(Collectors.toCollection(LinkedHashSet::new))); + return incident; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageOrgInfo.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageOrgInfo.java new file mode 100644 index 0000000..9eed55b --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/StatusPageOrgInfo.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.pojo.dto; + +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.entity.manager.StatusPageOrg; + +/** + * Manager-side status-page org DTO detached from JPA annotations. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class StatusPageOrgInfo { + + private Long id; + + @NotBlank + private String name; + + @NotBlank + private String description; + + @NotBlank + private String home; + + @NotBlank + private String logo; + + private String feedback; + + private String color; + + private byte state; + + private String creator; + + private String modifier; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; + + public static StatusPageOrgInfo fromEntity(StatusPageOrg statusPageOrg) { + if (statusPageOrg == null) { + return null; + } + StatusPageOrgInfo info = new StatusPageOrgInfo(); + info.setId(statusPageOrg.getId()); + info.setName(statusPageOrg.getName()); + info.setDescription(statusPageOrg.getDescription()); + info.setHome(statusPageOrg.getHome()); + info.setLogo(statusPageOrg.getLogo()); + info.setFeedback(statusPageOrg.getFeedback()); + info.setColor(statusPageOrg.getColor()); + info.setState(statusPageOrg.getState()); + info.setCreator(statusPageOrg.getCreator()); + info.setModifier(statusPageOrg.getModifier()); + info.setGmtCreate(statusPageOrg.getGmtCreate()); + info.setGmtUpdate(statusPageOrg.getGmtUpdate()); + return info; + } + + public StatusPageOrg toEntity() { + StatusPageOrg statusPageOrg = new StatusPageOrg(); + statusPageOrg.setId(id); + statusPageOrg.setName(name); + statusPageOrg.setDescription(description); + statusPageOrg.setHome(home); + statusPageOrg.setLogo(logo); + statusPageOrg.setFeedback(feedback); + statusPageOrg.setColor(color); + statusPageOrg.setState(state); + statusPageOrg.setCreator(creator); + statusPageOrg.setModifier(modifier); + statusPageOrg.setGmtCreate(gmtCreate); + statusPageOrg.setGmtUpdate(gmtUpdate); + return statusPageOrg; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/SystemConfig.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/SystemConfig.java new file mode 100644 index 0000000..daba718 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/SystemConfig.java @@ -0,0 +1,48 @@ +/* + * 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.manager.pojo.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * System Configuration + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SystemConfig { + + /** + * system time zone + */ + private String timeZoneId; + + /** + * system locale language region + */ + private String locale; + + /** + * layout ui theme + */ + private String theme; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/SystemSecret.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/SystemSecret.java new file mode 100644 index 0000000..b1c7bd5 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/SystemSecret.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.pojo.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * System Secret Config + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SystemSecret { + + /** + * secret key for jwt + */ + private String jwtSecret; + + /** + * secret key for aes + */ + private String aesSecret; +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/TemplateConfig.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/TemplateConfig.java new file mode 100644 index 0000000..f932ccf --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/TemplateConfig.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.pojo.dto; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * App Template Config + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TemplateConfig { + + /** + * app template config map + * key: app name + * value: app template config + */ + private Map<String, AppTemplate> apps; + + /** + * app template + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class AppTemplate { + + /** + * Is hide this app in main menus layout, only for app type, default true + */ + @Builder.Default + private boolean hide = true; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/TokenDto.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/TokenDto.java new file mode 100644 index 0000000..5a8fc65 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/TokenDto.java @@ -0,0 +1,41 @@ +/* + * 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.manager.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Refresh token dto + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Request refresh token transfer body") +public class TokenDto { + + @Schema(description = "token") + @NotBlank(message = "token can not null") + private String token; + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/AssignJobs.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/AssignJobs.java new file mode 100644 index 0000000..90f795c --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/AssignJobs.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.scheduler; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * List of assigned collection tasks + */ +@Slf4j +@Data +public class AssignJobs { + + /** + * current assign jobIds + */ + private Set<Long> jobs; + + /** + * jobs to be adding + */ + private Set<Long> addingJobs; + + /** + * jobs to be removed + */ + private Set<Long> removingJobs; + + /** + * jobs has pinned in this collector + */ + private Set<Long> pinnedJobs; + + public AssignJobs() { + jobs = ConcurrentHashMap.newKeySet(16); + addingJobs = ConcurrentHashMap.newKeySet(16); + removingJobs = ConcurrentHashMap.newKeySet(16); + pinnedJobs = ConcurrentHashMap.newKeySet(16); + } + + public void addAssignJob(Long jobId) { + jobs.add(jobId); + } + + public void addAddingJob(Long jobId) { + addingJobs.add(jobId); + } + + public void addRemovingJob(Long jobId) { + removingJobs.add(jobId); + } + + public void addPinnedJob(Long jobId) { + pinnedJobs.add(jobId); + } + + public void addAssignJobs(Set<Long> jobSet) { + if (jobSet != null && !jobSet.isEmpty()) { + jobs.addAll(jobSet); + } + } + + public void addAddingJobs(Set<Long> jobSet) { + if (jobSet != null && !jobSet.isEmpty()) { + addingJobs.addAll(jobSet); + } + } + + public void addRemovingJobs(Set<Long> jobSet) { + if (jobSet != null && !jobSet.isEmpty()) { + removingJobs.addAll(jobSet); + } + } + + public void addPinnedJobs(Set<Long> jobSet) { + if (jobSet != null && !jobSet.isEmpty()) { + pinnedJobs.addAll(jobSet); + } + } + + public void removeAssignJobs(Set<Long> jobIds) { + if (jobs == null || jobIds == null || jobIds.isEmpty()) { + return; + } + jobs.removeAll(jobIds); + } + + public void removeAddingJobs(Set<Long> jobIds) { + if (addingJobs == null || jobIds == null || jobIds.isEmpty()) { + return; + } + addingJobs.removeAll(jobIds); + } + + public void clearRemovingJobs() { + if (removingJobs == null) { + return; + } + removingJobs.clear(); + } + + /** + * Check if the corresponding jobId exists + * + * @param jobId jobId + * @return true if exists and removes the jobId from assignJobs + */ + public boolean containAndRemoveJob(Long jobId) { + if (jobs.isEmpty()) { + return false; + } + return jobs.remove(jobId); + } + + public void removeAddingJob(Long jobId) { + if (addingJobs == null || jobId == null) { + return; + } + addingJobs.remove(jobId); + } + + public void removeRemovingJob(Long jobId) { + if (removingJobs == null || jobId == null) { + return; + } + removingJobs.remove(jobId); + } + + public void removePinnedJob(Long jobId) { + if (pinnedJobs == null || jobId == null) { + return; + } + pinnedJobs.remove(jobId); + } + + /** + * Clean data + */ + public void clear() { + if (!jobs.isEmpty()) { + log.warn("assign jobs is not empty, maybe there are jobs not assigned"); + jobs.clear(); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/CollectJobScheduling.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/CollectJobScheduling.java new file mode 100644 index 0000000..55137e2 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/CollectJobScheduling.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.scheduler; + +import java.util.List; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * Collection job management provides api interface + */ +public interface CollectJobScheduling { + + /** + * Execute a one-time collection task and get the collected data response + * @param job Collect task details + * @return Collection results + */ + List<CollectRep.MetricsData> collectSyncJobData(Job job); + + /** + * Execute a one-time collection task and get the collected data response + * @param job Collect task details + * @param collector collector identity name + * @return Collection results + */ + List<CollectRep.MetricsData> collectSyncJobData(Job job, String collector); + + /** + * Issue periodic asynchronous collection tasks + * @param job Collect task details + * @param collector collector identity name + * @return long Job ID + */ + long addAsyncCollectJob(Job job, String collector); + + /** + * Update the periodic asynchronous collection tasks that have been delivered + * @param modifyJob Collect task details + * @return long Job ID + */ + long updateAsyncCollectJob(Job modifyJob); + + /** + * Update the periodic asynchronous collection tasks that have been delivered + * @param modifyJob Collect task details + * @param collector collector identity name + * @return long Job ID + */ + long updateAsyncCollectJob(Job modifyJob, String collector); + + /** + * Cancel periodic asynchronous collection tasks + * @param jobId Job ID + */ + void cancelAsyncCollectJob(Long jobId); + + /** + * one-time collect job response data + * @param metricsDataList collect data + */ + void collectSyncJobResponse(List<CollectRep.MetricsData> metricsDataList); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/CollectorJobScheduler.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/CollectorJobScheduler.java new file mode 100644 index 0000000..b9512f6 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/CollectorJobScheduler.java @@ -0,0 +1,476 @@ +/* + * 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.manager.scheduler; + +import com.google.protobuf.ByteString; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService; +import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectResponseEventListener; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.CollectorInfo; +import org.apache.hertzbeat.common.entity.dto.ServerInfo; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.RuntimeParamDefine; +import org.apache.hertzbeat.common.entity.manager.Collector; +import org.apache.hertzbeat.common.entity.manager.CollectorMonitorBind; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.AesUtil; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator; +import org.apache.hertzbeat.manager.dao.CollectorDao; +import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao; +import org.apache.hertzbeat.manager.dao.MonitorDao; +import org.apache.hertzbeat.manager.dao.ParamDao; +import org.apache.hertzbeat.manager.scheduler.netty.ManageServer; +import org.apache.hertzbeat.manager.service.AppService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.stereotype.Component; + +/** + * collector service + */ +@Component +@AutoConfigureAfter(value = {SchedulerProperties.class}) +@Slf4j +public class CollectorJobScheduler implements CollectorScheduling, CollectJobScheduling { + + private final Map<Long, Job> jobContentCache = new ConcurrentHashMap<>(16); + + private final Map<Long, CollectResponseEventListener> eventListeners = new ConcurrentHashMap<>(16); + + @Autowired + private CollectorDao collectorDao; + + @Autowired + private CollectorMonitorBindDao collectorMonitorBindDao; + + @Autowired + private ConsistentHash consistentHash; + + @Autowired + private CollectJobService collectJobService; + + @Autowired + private AppService appService; + + @Autowired + private MonitorDao monitorDao; + + @Autowired + private ParamDao paramDao; + + private ManageServer manageServer; + + @Override + public void collectorGoOnline(String identity, CollectorInfo collectorInfo) { + if (StringUtils.isBlank(identity)) { + log.error("identity can not be null if collector not existed"); + return; + } + Collector collector = collectorDao.findCollectorByName(identity).orElse(null); + if (Objects.nonNull(collector)) { + if (collector.getStatus() == CommonConstants.COLLECTOR_STATUS_ONLINE) { + return; + } + collector.setStatus(CommonConstants.COLLECTOR_STATUS_ONLINE); + if (collectorInfo != null) { + collector.setIp(collectorInfo.getIp()); + collector.setMode(collectorInfo.getMode()); + collector.setVersion(collectorInfo.getVersion()); + } + } else { + if (collectorInfo == null) { + log.error("collectorInfo can not null when collector not existed"); + return; + } + collector = Collector.builder() + .name(identity) + .ip(collectorInfo.getIp()) + .mode(collectorInfo.getMode()) + .version(collectorInfo.getVersion()) + .status(CommonConstants.COLLECTOR_STATUS_ONLINE) + .build(); + } + collectorDao.save(collector); + ConsistentHash.Node node = new ConsistentHash.Node(identity, collector.getMode(), + collector.getIp(), System.currentTimeMillis(), null); + consistentHash.addNode(node); + reBalanceCollectorAssignJobs(); + // Read database The fixed collection tasks at this collector are delivered + List<CollectorMonitorBind> binds = collectorMonitorBindDao.findCollectorMonitorBindsByCollector(identity); + if (CollectionUtils.isEmpty(binds)){ + return; + } + List<Monitor> monitors = monitorDao.findMonitorsByIdIn(binds.stream().map(CollectorMonitorBind::getMonitorId).collect(Collectors.toSet())); + for (Monitor monitor : monitors) { + if (Objects.isNull(monitor) || monitor.getStatus() == CommonConstants.MONITOR_PAUSED_CODE) { + continue; + } + try { + // build collect job entity + Job appDefine = appService.getAppDefine(monitor.getApp()); + if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) { + appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName()); + } + appDefine.setMonitorId(monitor.getId()); + appDefine.setDefaultInterval(monitor.getIntervals()); + appDefine.setCyclic(true); + appDefine.setTimestamp(System.currentTimeMillis()); + appDefine.setScheduleType(monitor.getScheduleType()); + appDefine.setCronExpression(monitor.getCronExpression()); + Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(), + CommonConstants.LABEL_INSTANCE, monitor.getInstance()); + appDefine.setMetadata(metadata); + List<Param> params = paramDao.findParamsByMonitorId(monitor.getId()); + List<Configmap> configmaps = params.stream() + .map(param -> Configmap.builder() + .key(param.getField()) + .value(param.getParamValue()) + .type(param.getType()).build()).collect(Collectors.toList()); + List<RuntimeParamDefine> paramDefaultValue = appDefine.getParams().stream() + .filter(item -> StringUtils.isNotBlank(item.getDefaultValue())) + .toList(); + paramDefaultValue.forEach(defaultVar -> { + if (configmaps.stream().noneMatch(item -> item.getKey().equals(defaultVar.getField()))) { + configmaps.add(Configmap.builder() + .key(defaultVar.getField()) + .value(defaultVar.getDefaultValue()) + .type((byte) 1) + .build()); + } + }); + appDefine.setConfigmap(configmaps); + long jobId = addAsyncCollectJob(appDefine, identity); + monitor.setJobId(jobId); + monitorDao.save(monitor); + } catch (Exception e) { + log.error("insert pinned monitor job: {} in collector: {} error,continue next monitor", monitor, identity, e); + } + } + } + + @Override + public void collectorGoOffline(String identity) { + Collector collector = collectorDao.findCollectorByName(identity).orElse(null); + if (Objects.isNull(collector)) { + log.info("the collector : {} not found.", identity); + return; + } + collector.setStatus(CommonConstants.COLLECTOR_STATUS_OFFLINE); + collectorDao.save(collector); + consistentHash.removeNode(identity); + reBalanceCollectorAssignJobs(); + log.info("the collector: {} go offline success.", identity); + } + + @Override + public void reBalanceCollectorAssignJobs() { + consistentHash.getAllNodes().entrySet().parallelStream().forEach(entry -> { + String collectorName = entry.getKey(); + AssignJobs assignJobs = entry.getValue().getAssignJobs(); + if (StringUtils.isBlank(collectorName) || Objects.isNull(assignJobs)) { + return; + } + if (CollectionUtils.isNotEmpty(assignJobs.getAddingJobs())) { + Set<Long> addedJobIds = new HashSet<>(8); + for (Long addingJobId : assignJobs.getAddingJobs()) { + Job job = jobContentCache.get(addingJobId); + if (Objects.isNull(job)) { + log.error("assigning job {} content is null.", addingJobId); + continue; + } + addedJobIds.add(addingJobId); + if (CommonConstants.MAIN_COLLECTOR_NODE.equals(collectorName)) { + collectJobService.addAsyncCollectJob(job); + } else { + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setDirection(ClusterMsg.Direction.REQUEST) + .setType(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK) + .setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(job))) + .build(); + this.manageServer.sendMsg(collectorName, message); + } + } + assignJobs.addAssignJobs(addedJobIds); + assignJobs.removeAddingJobs(addedJobIds); + } + if (CollectionUtils.isNotEmpty(assignJobs.getRemovingJobs())) { + if (CommonConstants.MAIN_COLLECTOR_NODE.equals(collectorName)) { + assignJobs.getRemovingJobs().forEach(jobId -> collectJobService.cancelAsyncCollectJob(jobId)); + } else { + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setDirection(ClusterMsg.Direction.REQUEST) + .setType(ClusterMsg.MessageType.DELETE_CYCLIC_TASK) + .setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(assignJobs.getRemovingJobs()))) + .build(); + this.manageServer.sendMsg(collectorName, message); + } + assignJobs.clearRemovingJobs(); + } + }); + } + + @Override + public boolean offlineCollector(String identity) { + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.GO_OFFLINE) + .setDirection(ClusterMsg.Direction.REQUEST) + .setIdentity(identity) + .build(); + ClusterMsg.Message response = this.manageServer.sendMsgSync(identity, message); + if (response == null || !String.valueOf(CommonConstants.SUCCESS_CODE).equals(response.getMsg().toStringUtf8())) { + return false; + } + log.info("send offline collector message to {} success", identity); + this.collectorGoOffline(identity); + return true; + } + + @Override + public boolean onlineCollector(String identity) { + Collector collector = collectorDao.findCollectorByName(identity).orElse(null); + if (Objects.isNull(collector)) { + return false; + } + ServerInfo serverInfo = ServerInfo.builder().aesSecret(AesUtil.getDefaultSecretKey()).build(); + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.GO_ONLINE) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(serverInfo))) + .setIdentity(identity) + .build(); + ClusterMsg.Message response = this.manageServer.sendMsgSync(identity, message); + if (response == null || !String.valueOf(CommonConstants.SUCCESS_CODE).equals(response.getMsg().toStringUtf8())) { + return false; + } + log.info("send online collector message to {} success", identity); + CollectorInfo collectorInfo = CollectorInfo.builder() + .name(collector.getName()) + .ip(collector.getIp()) + .version(collector.getVersion()) + .mode(collector.getMode()) + .build(); + this.collectorGoOnline(identity, collectorInfo); + return true; + } + + @Override + public List<CollectRep.MetricsData> collectSyncJobData(Job job) { + // todo dispatchKey ip+port or id + String dispatchKey = String.valueOf(job.getMonitorId()); + ConsistentHash.Node node = consistentHash.preDispatchJob(dispatchKey); + if (Objects.isNull(node)) { + log.error("there is no collector online to assign job."); + CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() + .setCode(CollectRep.Code.FAIL) + .setMsg("no collector online to assign job") + .build(); + return Collections.singletonList(metricsData); + } + if (CommonConstants.MAIN_COLLECTOR_NODE.equals(node.getIdentity())) { + return collectJobService.collectSyncJobData(job); + } else { + List<CollectRep.MetricsData> metricsData = new LinkedList<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.ISSUE_ONE_TIME_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(job))) + .build(); + boolean result = this.manageServer.sendMsg(node.getIdentity(), message); + + if (result) { + CollectResponseEventListener listener = new CollectResponseEventListener() { + @Override + public void response(List<CollectRep.MetricsData> responseMetrics) { + if (responseMetrics != null) { + metricsData.addAll(responseMetrics); + } + countDownLatch.countDown(); + } + }; + eventListeners.put(job.getMonitorId(), listener); + } + try { + countDownLatch.await(120, TimeUnit.SECONDS); + } catch (Exception e) { + log.info("The sync task runs for 120 seconds with no response and returns"); + } + return metricsData; + } + } + + @Override + public List<CollectRep.MetricsData> collectSyncJobData(Job job, String collector) { + ConsistentHash.Node node = consistentHash.getNode(collector); + if (Objects.isNull(node)) { + log.error("there is no collector online to assign job."); + CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() + .setCode(CollectRep.Code.FAIL) + .setMsg("the collector is offline and cannot assign job") + .build(); + return Collections.singletonList(metricsData); + } + if (CommonConstants.MAIN_COLLECTOR_NODE.equals(node.getIdentity())) { + return collectJobService.collectSyncJobData(job); + } + List<CollectRep.MetricsData> metricsData = new LinkedList<>(); + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.ISSUE_ONE_TIME_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(job))) + .build(); + boolean result = this.manageServer.sendMsg(node.getIdentity(), message); + if (result) { + CountDownLatch countDownLatch = new CountDownLatch(1); + CollectResponseEventListener listener = new CollectResponseEventListener() { + @Override + public void response(List<CollectRep.MetricsData> responseMetrics) { + if (responseMetrics != null) { + metricsData.addAll(responseMetrics); + } + countDownLatch.countDown(); + } + }; + eventListeners.put(job.getMonitorId(), listener); + try { + countDownLatch.await(120, TimeUnit.SECONDS); + } catch (Exception e) { + log.info("The sync task runs for 120 seconds with no response and returns"); + } + } + return metricsData; + } + + @Override + public long addAsyncCollectJob(Job job, String collector) { + long jobId = SnowFlakeIdGenerator.generateId(); + job.setId(jobId); + jobContentCache.put(jobId, job); + ConsistentHash.Node node; + if (StringUtils.isBlank(collector)) { + // todo dispatchKey ip+port or id + String dispatchKey = String.valueOf(job.getMonitorId()); + node = consistentHash.dispatchJob(dispatchKey, jobId); + if (node == null) { + log.error("there is no collector online to assign job."); + return jobId; + } + } else { + node = consistentHash.getNode(collector); + if (node == null) { + log.error("there is no collector name: {} online to assign job.", collector); + return jobId; + } + node.getAssignJobs().addPinnedJob(jobId); + } + if (CommonConstants.MAIN_COLLECTOR_NODE.equals(node.getIdentity())) { + collectJobService.addAsyncCollectJob(job); + } else { + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(job))) + .build(); + this.manageServer.sendMsg(node.getIdentity(), message); + } + return jobId; + } + + @Override + public long updateAsyncCollectJob(Job modifyJob) { + // delete and add + long preJobId = modifyJob.getId(); + long newJobId = addAsyncCollectJob(modifyJob, null); + jobContentCache.remove(preJobId); + cancelAsyncCollectJob(preJobId); + return newJobId; + } + + @Override + public long updateAsyncCollectJob(Job modifyJob, String collector) { + // delete and add + long preJobId = modifyJob.getId(); + long newJobId = addAsyncCollectJob(modifyJob, collector); + jobContentCache.remove(preJobId); + cancelAsyncCollectJob(preJobId); + return newJobId; + } + + @Override + public void cancelAsyncCollectJob(Long jobId) { + if (jobId == null) { + return; + } + jobContentCache.remove(jobId); + for (ConsistentHash.Node node : consistentHash.getAllNodes().values()) { + AssignJobs assignJobs = node.getAssignJobs(); + if (assignJobs.getPinnedJobs().remove(jobId) + || assignJobs.getJobs().remove(jobId) || assignJobs.getAddingJobs().remove(jobId)) { + node.removeVirtualNodeJob(jobId); + if (CommonConstants.MAIN_COLLECTOR_NODE.equals(node.getIdentity())) { + collectJobService.cancelAsyncCollectJob(jobId); + } else { + ClusterMsg.Message deleteMessage = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.DELETE_CYCLIC_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(List.of(jobId)))) + .build(); + this.manageServer.sendMsg(node.getIdentity(), deleteMessage); + } + // break; if is there jod exist in multi collector? + } + } + } + + @Override + public void collectSyncJobResponse(List<CollectRep.MetricsData> metricsDataList) { + if (CollectionUtils.isEmpty(metricsDataList)) { + return; + } + CollectRep.MetricsData metricsData = metricsDataList.get(0); + long monitorId = metricsData.getId(); + CollectResponseEventListener eventListener = eventListeners.remove(monitorId); + if (Objects.nonNull(eventListener)) { + eventListener.response(metricsDataList); + } + } + + public void setManageServer(ManageServer manageServer) { + this.manageServer = manageServer; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/CollectorScheduling.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/CollectorScheduling.java new file mode 100644 index 0000000..76ad110 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/CollectorScheduling.java @@ -0,0 +1,58 @@ +/* + * 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.manager.scheduler; + +import org.apache.hertzbeat.common.entity.dto.CollectorInfo; + +/** + * slave collector service + */ +public interface CollectorScheduling { + + /** + * register collector go online + * @param identity collector identity name + * @param collectorInfo collector information + */ + void collectorGoOnline(String identity, CollectorInfo collectorInfo); + + /** + * register collector go offline + * @param identity collector identity name + */ + void collectorGoOffline(String identity); + + /** + * reBalance dispatch monitoring jobs when collector go online or offline or timeout + */ + void reBalanceCollectorAssignJobs(); + + /** + * offline collector(stop collector collect operation) + * @param identity collector identity name + * @return true/false + */ + boolean offlineCollector(String identity); + + /** + * online collector(start collector collect operation) + * @param identity collector identity name + * @return true/false + */ + boolean onlineCollector(String identity); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/ConcurrentTreeMap.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/ConcurrentTreeMap.java new file mode 100644 index 0000000..7cba010 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/ConcurrentTreeMap.java @@ -0,0 +1,110 @@ +/* + * 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.manager.scheduler; + +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * concurrent treemap + */ +public class ConcurrentTreeMap<K, V> extends TreeMap<K, V> { + + private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + + public ConcurrentTreeMap() { + super(); + } + + @Override + public V put(K key, V value) { + readWriteLock.writeLock().lock(); + try { + return super.put(key, value); + } finally { + readWriteLock.writeLock().unlock(); + } + } + + @Override + public V remove(Object key) { + readWriteLock.writeLock().lock(); + try { + return super.remove(key); + } finally { + readWriteLock.writeLock().unlock(); + } + } + + @Override + public Map.Entry<K, V> firstEntry() { + readWriteLock.readLock().lock(); + try { + return super.firstEntry(); + } finally { + readWriteLock.readLock().unlock(); + } + } + + @Override + public Map.Entry<K, V> higherEntry(K key) { + readWriteLock.readLock().lock(); + try { + return super.higherEntry(key); + } finally { + readWriteLock.readLock().unlock(); + } + } + + public Map.Entry<K, V> higherOrFirstEntry(K key){ + readWriteLock.readLock().lock(); + try { + Map.Entry<K, V> entry = super.higherEntry(key); + if (entry == null) { + return super.firstEntry(); + } + return entry; + } finally { + readWriteLock.readLock().unlock(); + } + } + + @Override + public Map.Entry<K, V> ceilingEntry(K key) { + readWriteLock.readLock().lock(); + try { + return super.ceilingEntry(key); + } finally { + readWriteLock.readLock().unlock(); + } + } + + public Map.Entry<K, V> ceilingOrFirstEntry(K key) { + readWriteLock.readLock().lock(); + try { + Map.Entry<K, V> entry = super.ceilingEntry(key); + if (entry == null) { + return super.firstEntry(); + } + return entry; + } finally { + readWriteLock.readLock().unlock(); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/ConsistentHash.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/ConsistentHash.java new file mode 100644 index 0000000..44d1707 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/ConsistentHash.java @@ -0,0 +1,456 @@ +/* + * 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.manager.scheduler; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.constants.CommonConstants; + +/** + * Collector and task mapping scheduling implemented by consistent hashing + */ +@Slf4j +public class ConsistentHash { + + /** + * consistent hash circle + */ + private final ConcurrentTreeMap<Integer, Node> hashCircle; + + /** + * collector node + */ + private final Map<String, Node> existNodeMap; + + /** + * not dispatched job cache + */ + private final List<DispatchJob> dispatchJobCache; + + /** + * Default number of VM nodes + */ + private static final byte VIRTUAL_NODE_DEFAULT_SIZE = 10; + + public ConsistentHash() { + hashCircle = new ConcurrentTreeMap<>(); + existNodeMap = new ConcurrentHashMap<>(16); + dispatchJobCache = Collections.synchronizedList(new LinkedList<>()); + } + + /** + * add virtual node + * @param newNode node + * @param identity virtual node identity + */ + public synchronized void addVirtualNode(Node newNode, String identity){ + int virtualHashKey = hash(identity); + hashCircle.put(virtualHashKey, newNode); + newNode.addVirtualNodeJobs(virtualHashKey, ConcurrentHashMap.newKeySet(16)); + Map.Entry<Integer, Node> higherVirtualNode = hashCircle.higherOrFirstEntry(virtualHashKey); + // Reassign tasks that are routed to the higherVirtualNode virtual node + // Tasks are either on the original virtual node or on the new virtual node + Integer higherVirtualNodeKey = higherVirtualNode.getKey(); + Node higherNode = higherVirtualNode.getValue(); + Set<Long[]> dispatchJobs = higherNode.clearVirtualNodeJobs(higherVirtualNodeKey); + if (dispatchJobs != null && !dispatchJobs.isEmpty()) { + Set<Long[]> reDispatchJobs = ConcurrentHashMap.newKeySet(dispatchJobs.size()); + Iterator<Long[]> iterator = dispatchJobs.iterator(); + while (iterator.hasNext()) { + Long[] jobHash = iterator.next(); + int dispatchHash = jobHash[1].intValue(); + if (dispatchHash <= virtualHashKey) { + reDispatchJobs.add(jobHash); + iterator.remove(); + } + } + higherNode.virtualNodeMap.put(higherVirtualNodeKey, dispatchJobs); + Set<Long> jobIds = reDispatchJobs.stream().map(item -> item[0]).collect(Collectors.toSet()); + newNode.addVirtualNodeJobs(virtualHashKey, reDispatchJobs); + if (higherNode != newNode) { + higherNode.assignJobs.removeAssignJobs(jobIds); + higherNode.assignJobs.addRemovingJobs(jobIds); + newNode.assignJobs.addAddingJobs(jobIds); + } + } + } + + /** + * add collector node + * @param newNode node + */ + public void addNode(Node newNode) { + // when mode is cluster public, need reBalance dispatch jobs. else not when is cloud-edge private + if (!CommonConstants.MODE_PRIVATE.equals(newNode.mode)) { + byte virtualNodeNum = newNode.quality == null ? VIRTUAL_NODE_DEFAULT_SIZE : newNode.quality; + for (byte i = 0; i < virtualNodeNum; i++) { + addVirtualNode(newNode, newNode.identity + i); + } + } + existNodeMap.put(newNode.identity, newNode); + dispatchJobInCache(); + } + + /** + * remove virtual node + * @param deletedNode node + * @param virtualNodeHash virtual node hash key + */ + public synchronized void removeVirtualNode(Node deletedNode, Integer virtualNodeHash) { + Set<Long[]> removeJobHashSet = deletedNode.virtualNodeMap.get(virtualNodeHash); + // Migrate the virtualNodeEntry collection task to the nearest virtual node that is larger than it + hashCircle.remove(virtualNodeHash); + if (removeJobHashSet == null || removeJobHashSet.isEmpty()) { + return; + } + Map.Entry<Integer, Node> higherVirtualEntry = hashCircle.higherOrFirstEntry(virtualNodeHash); + if (higherVirtualEntry == null || higherVirtualEntry.getValue() == deletedNode) { + higherVirtualEntry = null; + } + // jobId + Set<Long> removeJobIds = removeJobHashSet.stream().map(item -> item[0]).collect(Collectors.toSet()); + deletedNode.assignJobs.removeAssignJobs(removeJobIds); + deletedNode.assignJobs.addRemovingJobs(removeJobIds); + if (higherVirtualEntry == null) { + // jobId-dispatchHash + removeJobHashSet.forEach(value -> { + Long jobId = value[0]; + Integer dispatchHash = value[1].intValue(); + if (removeJobIds.contains(jobId)) { + dispatchJobCache.add(new DispatchJob(dispatchHash, jobId)); + } else { + log.error("Get job {} from removeJobMap null.", jobId); + } + }); + } else { + Node higherVirtualNode = higherVirtualEntry.getValue(); + higherVirtualNode.addVirtualNodeJobs(higherVirtualEntry.getKey(), removeJobHashSet); + higherVirtualNode.assignJobs.addAddingJobs(removeJobIds); + } + } + + /** + * deleted collector node + * @param name collector name + */ + public Node removeNode(String name) { + Node deletedNode = existNodeMap.remove(name); + if (deletedNode == null) { + return null; + } + for (Integer virtualNodeHash : deletedNode.virtualNodeMap.keySet()) { + removeVirtualNode(deletedNode, virtualNodeHash); + } + deletedNode.destroy(); + dispatchJobInCache(); + return deletedNode; + } + + public synchronized void dispatchJobInCache() { + if (!dispatchJobCache.isEmpty()) { + int size = dispatchJobCache.size(); + for (int index = 0; index < size; index++) { + DispatchJob dispatchJob = dispatchJobCache.remove(0); + dispatchJob(dispatchJob.dispatchHash, dispatchJob.jobId, false); + } + } + } + + /** + * get all collector nodes + * @return nodes + */ + public Map<String, Node> getAllNodes() { + return existNodeMap; + } + + /** + * get node + * @param collectorName collector name + * @return node + */ + public Node getNode(String collectorName) { + return existNodeMap.get(collectorName); + } + + /** + * Obtain the cached collection scheduling task + * @return cache task + */ + public List<DispatchJob> getDispatchJobCache() { + return dispatchJobCache; + } + + /** + * obtain the collector node according to the collection task information + * + * @param dispatchKey collector task route key: ip+appId + * @param jobId jobId + * @return collector node + */ + public Node dispatchJob(String dispatchKey, Long jobId) { + if (dispatchKey == null || StringUtils.isBlank(dispatchKey)) { + log.error("The dispatch key can not null."); + return null; + } + int dispatchHash = hash(dispatchKey); + return dispatchJob(dispatchHash, jobId, true); + } + + /** + * The collector node to which the collector is assigned is obtained in advance based on the collection task information + * + * @param dispatchKey collector task route key: ip+appId + * @return collector node + */ + public Node preDispatchJob(String dispatchKey) { + if (dispatchKey == null || StringUtils.isBlank(dispatchKey)) { + log.error("The dispatch key can not null."); + return null; + } + int dispatchHash = hash(dispatchKey); + return preDispatchJob(dispatchHash); + } + + /** + * Obtain the collector node to which the collector is assigned based on the collection task information + * + * @param dispatchHash The task route hash is collected + * @param jobId jobId + * @param isFlushed is has flush this job or wait to dispatch + * @return collector node + */ + public Node dispatchJob(Integer dispatchHash, Long jobId, boolean isFlushed) { + if (dispatchHash == null || hashCircle == null || hashCircle.isEmpty()) { + log.warn("There is no available collector registered. Cache the job {}.", jobId); + dispatchJobCache.add(new DispatchJob(dispatchHash, jobId)); + return null; + } + Map.Entry<Integer, Node> ceilEntry = hashCircle.ceilingOrFirstEntry(dispatchHash); + int virtualKey = ceilEntry.getKey(); + Node curNode = ceilEntry.getValue(); + + curNode.addJob(virtualKey, dispatchHash, jobId, isFlushed); + return curNode; + } + + /** + * The collector node to which the collector is assigned is obtained in advance based on the collection task information + * + * @param dispatchHash The task route hash is collected + * @return collector node + */ + public Node preDispatchJob(Integer dispatchHash) { + if (dispatchHash == null || hashCircle == null || hashCircle.isEmpty()) { + log.warn("There is no available collector registered."); + return null; + } + Map.Entry<Integer, Node> ceilEntry = hashCircle.ceilingOrFirstEntry(dispatchHash); + return ceilEntry.getValue(); + } + + /** + * hash long + * @param key long value + * @return hash value + */ + private int hash(long key) { + String keyStr = String.valueOf(key); + return hash(keyStr); + } + + /** + * FNV1_32_HASH algorithm + * @param key the key + * @return hash + */ + private int hash(String key) { + final int p = 16777619; + int hash = (int) 2166136261L; + for (int i = 0; i < key.length(); i++) { + hash = (hash ^ key.charAt(i)) * p; + } + hash += hash << 13; + hash ^= hash >> 7; + hash += hash << 3; + hash ^= hash >> 17; + hash += hash << 5; + // Negative numbers take their absolute value + if (hash < 0) { + hash = Math.abs(hash); + } + return hash; + } + + + /** + * dispatch job summary + */ + @AllArgsConstructor + public static class DispatchJob { + + /** + * dispatch task route key + */ + private Integer dispatchHash; + + /** + * job ID + */ + @Getter + private Long jobId; + } + + /** + * collector node machine address + */ + public static class Node { + + /** + * collector identity + */ + @Getter + private final String identity; + + /** + * collector mode: public or private + */ + private final String mode; + + /** + * ip + */ + private final String ip; + + /** + * collector On-line time stamp + */ + private final long uptime; + + /** + * collector's own performance service quality score 0 - 127 + * The number of virtual nodes will be calculated based on this service quality score + * + */ + private final Byte quality; + + /** + * use this collector's collect job ID list + * jobId,jobVersion + */ + private AssignJobs assignJobs; + + /** + * the collection task ID list mapped by each virtual node corresponding to this node + * Long[] [0]-jobId, [1]-dispatchHash + */ + private Map<Integer, Set<Long[]>> virtualNodeMap; + + public Node(String identity, String mode, String ip, long uptime, Byte quality) { + this.identity = identity; + this.mode = mode; + this.ip = ip; + this.uptime = uptime; + this.quality = quality; + assignJobs = new AssignJobs(); + virtualNodeMap = new ConcurrentHashMap<>(VIRTUAL_NODE_DEFAULT_SIZE); + } + + private synchronized void addJob(Integer virtualNodeKey, Integer dispatchHash, Long jobId, boolean isFlushed) { + if (virtualNodeMap == null) { + virtualNodeMap = new ConcurrentHashMap<>(VIRTUAL_NODE_DEFAULT_SIZE); + } + if (assignJobs == null) { + assignJobs = new AssignJobs(); + } + Set<Long[]> virtualNodeJob = virtualNodeMap.computeIfAbsent(virtualNodeKey, k -> ConcurrentHashMap.newKeySet(16)); + virtualNodeJob.add(new Long[]{jobId, dispatchHash.longValue()}); + if (isFlushed) { + assignJobs.addAssignJob(jobId); + } else { + assignJobs.addAddingJob(jobId); + } + } + + /** + * obtain the collection task routed by the specified virtual node according to virtualNodeKey + * @param virtualNodeKey virtualNodeKey + * @return collection task + */ + private Set<Long[]> clearVirtualNodeJobs(Integer virtualNodeKey) { + if (virtualNodeMap == null || virtualNodeMap.isEmpty()) { + return null; + } + Set<Long[]> virtualNodeJobs = virtualNodeMap.remove(virtualNodeKey); + virtualNodeMap.put(virtualNodeKey, ConcurrentHashMap.newKeySet(16)); + return virtualNodeJobs; + } + + private void addVirtualNodeJobs(Integer virtualHashKey, Set<Long[]> reDispatchJobs) { + if (reDispatchJobs == null) { + return; + } + if (virtualNodeMap == null) { + virtualNodeMap = new ConcurrentHashMap<>(16); + } + virtualNodeMap.computeIfPresent(virtualHashKey, (k, v) -> { + reDispatchJobs.addAll(v); + return v; + }); + virtualNodeMap.put(virtualHashKey, reDispatchJobs); + } + + public void removeVirtualNodeJob(Long jobId) { + if (jobId == null || virtualNodeMap == null) { + return; + } + for (Set<Long[]> jobSet : virtualNodeMap.values()) { + Optional<Long[]> optional = jobSet.stream().filter(item -> Objects.equals(item[0], jobId)).findFirst(); + if (optional.isPresent()) { + jobSet.remove(optional.get()); + break; + } + } + } + + public AssignJobs getAssignJobs() { + return assignJobs; + } + + public void destroy() { + if (assignJobs != null) { + assignJobs.clear(); + } + if (virtualNodeMap != null) { + virtualNodeMap.clear(); + } + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/ManagerWorkerPool.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/ManagerWorkerPool.java new file mode 100644 index 0000000..0c51637 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/ManagerWorkerPool.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.scheduler; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.concurrent.ManagedExecutor; +import org.apache.hertzbeat.common.concurrent.ManagedExecutors; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * manager module thread pool + */ +@Slf4j +@Component +public class ManagerWorkerPool implements DisposableBean { + private final ManagedExecutor workerExecutor; + + private final ManagedExecutor longRunningExecutor; + + public ManagerWorkerPool() { + this(VirtualThreadProperties.defaults()); + } + + @Autowired + public ManagerWorkerPool(VirtualThreadProperties virtualThreadProperties) { + VirtualThreadProperties properties = + virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties; + this.workerExecutor = createWorkerExecutor(properties); + this.longRunningExecutor = createLongRunningExecutor(properties, workerExecutor); + } + + private ManagedExecutor createWorkerExecutor(VirtualThreadProperties properties) { + Thread.UncaughtExceptionHandler handler = (thread, throwable) -> { + log.error("workerExecutor has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }; + if (properties.enabled()) { + VirtualThreadProperties.PoolProperties poolProperties = properties.manager(); + return ManagedExecutors.newVirtualExecutor("manager-worker", "manager-worker-", + poolProperties.mode(), poolProperties.maxConcurrentJobs(), handler); + } + return ManagedExecutors.wrap("manager-worker", createLegacyExecutor(handler)); + } + + private ManagedExecutor createLongRunningExecutor(VirtualThreadProperties properties, ManagedExecutor fallback) { + if (!properties.enabled()) { + return fallback; + } + return ManagedExecutors.newPlatformExecutor("manager-long-running", "manager-long-running-", + (thread, throwable) -> { + log.error("manager longRunningExecutor has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }); + } + + private ExecutorService createLegacyExecutor(Thread.UncaughtExceptionHandler handler) { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler(handler) + .setDaemon(true) + .setNameFormat("manager-worker-%d") + .build(); + return new ThreadPoolExecutor(6, + 10, + 10, + TimeUnit.SECONDS, + new SynchronousQueue<>(), + threadFactory, + new ThreadPoolExecutor.AbortPolicy()); + } + + public void executeJob(Runnable runnable) throws RejectedExecutionException { + workerExecutor.execute(runnable); + } + + /** + * Run a long-lived task outside of the short-task execution lane. + * + * @param runnable task + */ + public void executeLongRunning(Runnable runnable) { + longRunningExecutor.execute(runnable); + } + + @Override + public void destroy() throws Exception { + workerExecutor.close(); + if (longRunningExecutor != workerExecutor) { + longRunningExecutor.close(); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/SchedulerInit.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/SchedulerInit.java new file mode 100644 index 0000000..d91963c --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/SchedulerInit.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.scheduler; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.CollectorInfo; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.RuntimeParamDefine; +import org.apache.hertzbeat.common.entity.manager.Collector; +import org.apache.hertzbeat.common.entity.manager.CollectorMonitorBind; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.apache.hertzbeat.manager.config.PrometheusProxyConfig; +import org.apache.hertzbeat.manager.dao.CollectorDao; +import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao; +import org.apache.hertzbeat.manager.dao.MonitorDao; +import org.apache.hertzbeat.manager.dao.ParamDao; +import org.apache.hertzbeat.manager.service.AppService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.util.StringUtils; + +/** + * scheduler init + */ +@Configuration +@Order(value = Ordered.LOWEST_PRECEDENCE - 1) +@Slf4j +public class SchedulerInit implements CommandLineRunner { + + @Autowired + private CollectorScheduling collectorScheduling; + + @Autowired + private CollectJobScheduling collectJobScheduling; + + private static final String MAIN_COLLECTOR_NODE_IP = "127.0.0.1"; + private static final String DEFAULT_COLLECTOR_VERSION = "DEBUG"; + public static final String PARAM_FIELD_PORT = "port"; + + @Autowired + private AppService appService; + + @Autowired + private MonitorDao monitorDao; + + @Autowired + private ParamDao paramDao; + + @Autowired + private CollectorDao collectorDao; + + @Autowired + private CollectorMonitorBindDao collectorMonitorBindDao; + + @Autowired + private PrometheusProxyConfig prometheusProxyConfig; + + @Override + public void run(String... args) throws Exception { + // init pre collector status + List<Collector> collectors = collectorDao.findAll().stream() + .peek(item -> item.setStatus(CommonConstants.COLLECTOR_STATUS_OFFLINE)) + .collect(Collectors.toList()); + collectorDao.saveAll(collectors); + // insert default consistent node + CollectorInfo collectorInfo = CollectorInfo.builder() + .name(CommonConstants.MAIN_COLLECTOR_NODE) + .ip(MAIN_COLLECTOR_NODE_IP) + .version(DEFAULT_COLLECTOR_VERSION) + .build(); + collectorScheduling.collectorGoOnline(CommonConstants.MAIN_COLLECTOR_NODE, collectorInfo); + // init jobs + List<Monitor> monitors = monitorDao.findMonitorsByStatusNotInAndJobIdNotNull(List.of(CommonConstants.MONITOR_PAUSED_CODE)); + List<CollectorMonitorBind> monitorBinds = collectorMonitorBindDao.findAll(); + Map<Long, String> monitorIdCollectorMap = monitorBinds.stream().collect( + Collectors.toMap(CollectorMonitorBind::getMonitorId, CollectorMonitorBind::getCollector)); + for (Monitor monitor : monitors) { + try { + // build collect job entity + boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape()); + String app = isStatic ? monitor.getApp() : monitor.getScrape(); + Job appDefine = appService.getAppDefine(app); + // set Prometheus proxy mode + appDefine.setPrometheusProxyMode(prometheusProxyConfig.isPrometheusProxy()); + if (!isStatic) { + appDefine.setSd(true); + } + List<Param> params = paramDao.findParamsByMonitorId(monitor.getId()); + if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) { + appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName()); + } + appDefine.setId(monitor.getJobId()); + appDefine.setMonitorId(monitor.getId()); + appDefine.setDefaultInterval(monitor.getIntervals()); + appDefine.setCyclic(true); + appDefine.setTimestamp(System.currentTimeMillis()); + appDefine.setScheduleType(monitor.getScheduleType()); + appDefine.setCronExpression(monitor.getCronExpression()); + + String instance = monitor.getInstance(); + + Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(), + CommonConstants.LABEL_INSTANCE, instance); + appDefine.setMetadata(metadata); + appDefine.setLabels(monitor.getLabels()); + appDefine.setAnnotations(monitor.getAnnotations()); + List<Configmap> configmaps = params.stream() + .map(param -> new Configmap(param.getField(), param.getParamValue(), + param.getType())).collect(Collectors.toList()); + List<RuntimeParamDefine> paramDefaultValue = appDefine.getParams().stream() + .filter(item -> StringUtils.hasText(item.getDefaultValue())) + .toList(); + paramDefaultValue.forEach(defaultVar -> { + if (configmaps.stream().noneMatch(item -> item.getKey().equals(defaultVar.getField()))) { + Configmap configmap = new Configmap(defaultVar.getField(), defaultVar.getDefaultValue(), CommonConstants.TYPE_STRING); + configmaps.add(configmap); + } + }); + appDefine.setConfigmap(configmaps); + String collector = monitorIdCollectorMap.get(monitor.getId()); + long jobId = collectJobScheduling.addAsyncCollectJob(appDefine, collector); + monitor.setJobId(jobId); + monitorDao.save(monitor); + } catch (Exception e) { + log.error("init monitor job: {} error,continue next monitor", monitor, e); + } + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/SchedulerProperties.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/SchedulerProperties.java new file mode 100644 index 0000000..826a804 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/SchedulerProperties.java @@ -0,0 +1,59 @@ +/* + * 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.manager.scheduler; + +import lombok.Getter; +import lombok.Setter; +import org.apache.hertzbeat.common.constants.ConfigConstants; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * scheduler properties config + */ + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = + ConfigConstants.FunctionModuleConstants.SCHEDULER) +public class SchedulerProperties { + + private ServerProperties server; + + /** + * server properties + */ + @Getter + @Setter + public static class ServerProperties { + + private boolean enabled = true; + + private int port = 1158; + + /** + * an IdleStateEvent whose state is IdleState.ALL_IDLE will be triggered when neither read nor write + * was performed for the specified period of time. + * unit: s + */ + private int idleStateEventTriggerTime = 100; + + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/ManageServer.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/ManageServer.java new file mode 100644 index 0000000..8895cbb --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/ManageServer.java @@ -0,0 +1,304 @@ +/* + * 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.manager.scheduler.netty; + +import io.netty.channel.Channel; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.calculate.CollectorAlertHandler; +import org.apache.hertzbeat.common.concurrent.BackgroundTaskExecutor; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.manager.scheduler.CollectorJobScheduler; +import org.apache.hertzbeat.manager.scheduler.SchedulerProperties; +import org.apache.hertzbeat.manager.scheduler.netty.process.CollectCyclicDataResponseProcessor; +import org.apache.hertzbeat.manager.scheduler.netty.process.CollectCyclicServiceDiscoveryDataResponseProcessor; +import org.apache.hertzbeat.manager.scheduler.netty.process.CollectOneTimeDataResponseProcessor; +import org.apache.hertzbeat.manager.scheduler.netty.process.CollectorOfflineProcessor; +import org.apache.hertzbeat.manager.scheduler.netty.process.CollectorOnlineProcessor; +import org.apache.hertzbeat.manager.scheduler.netty.process.HeartbeatProcessor; +import org.apache.hertzbeat.remoting.RemotingServer; +import org.apache.hertzbeat.remoting.event.NettyEventListener; +import org.apache.hertzbeat.remoting.netty.NettyRemotingServer; +import org.apache.hertzbeat.remoting.netty.NettyServerConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * manage server + */ +@Component +@Order(value = Ordered.LOWEST_PRECEDENCE) +@ConditionalOnProperty(prefix = "scheduler.server", + name = "enabled", havingValue = "true") +@Slf4j +public class ManageServer implements CommandLineRunner { + + private final CollectorJobScheduler collectorJobScheduler; + + private final CollectorAlertHandler collectorAlertHandler; + + private final CommonDataQueue commonDataQueue; + + private ScheduledExecutorService channelSchedule; + + private final ExecutorService channelCheckExecutor; + + private final Object channelCheckLock = new Object(); + + private boolean channelCheckRunning; + + private boolean channelCheckPending; + + private RemotingServer remotingServer; + + private final Map<String, Channel> clientChannelTable = new ConcurrentHashMap<>(16); + + public ManageServer(final SchedulerProperties schedulerProperties, + final CollectorJobScheduler collectorJobScheduler, + final BackgroundTaskExecutor threadPool, + final CollectorAlertHandler collectorAlertHandler, + final CommonDataQueue commonDataQueue) { + this(schedulerProperties, collectorJobScheduler, threadPool, collectorAlertHandler, commonDataQueue, + VirtualThreadProperties.defaults()); + } + + @Autowired + public ManageServer(final SchedulerProperties schedulerProperties, + final CollectorJobScheduler collectorJobScheduler, + final BackgroundTaskExecutor threadPool, + final CollectorAlertHandler collectorAlertHandler, + final CommonDataQueue commonDataQueue, + final VirtualThreadProperties virtualThreadProperties) { + this.collectorJobScheduler = collectorJobScheduler; + this.collectorJobScheduler.setManageServer(this); + this.collectorAlertHandler = collectorAlertHandler; + this.commonDataQueue = commonDataQueue; + this.channelCheckExecutor = createChannelCheckExecutor(virtualThreadProperties); + this.init(schedulerProperties, threadPool); + } + + private void init(final SchedulerProperties schedulerProperties, final BackgroundTaskExecutor threadPool) { + NettyServerConfig nettyServerConfig = new NettyServerConfig(); + nettyServerConfig.setPort(schedulerProperties.getServer().getPort()); + nettyServerConfig.setIdleStateEventTriggerTime(schedulerProperties.getServer().getIdleStateEventTriggerTime()); + NettyEventListener nettyEventListener = new ManageNettyEventListener(); + this.remotingServer = new NettyRemotingServer(nettyServerConfig, nettyEventListener, threadPool); + + // register processor + this.remotingServer.registerProcessor(ClusterMsg.MessageType.HEARTBEAT, new HeartbeatProcessor(this)); + this.remotingServer.registerProcessor(ClusterMsg.MessageType.GO_ONLINE, new CollectorOnlineProcessor(this)); + this.remotingServer.registerProcessor(ClusterMsg.MessageType.GO_OFFLINE, new CollectorOfflineProcessor(this)); + this.remotingServer.registerProcessor(ClusterMsg.MessageType.RESPONSE_ONE_TIME_TASK_DATA, new CollectOneTimeDataResponseProcessor(this)); + this.remotingServer.registerProcessor(ClusterMsg.MessageType.RESPONSE_CYCLIC_TASK_DATA, + new CollectCyclicDataResponseProcessor(commonDataQueue)); + this.remotingServer.registerProcessor(ClusterMsg.MessageType.RESPONSE_CYCLIC_TASK_SD_DATA, + new CollectCyclicServiceDiscoveryDataResponseProcessor(commonDataQueue)); + + this.channelSchedule = Executors.newSingleThreadScheduledExecutor(); + } + + public void start() { + this.remotingServer.start(); + + this.channelSchedule.scheduleAtFixedRate(this::dispatchChannelHealthCheck, 10, 3, TimeUnit.SECONDS); + } + + public void shutdown() { + this.remotingServer.shutdown(); + + if (this.channelSchedule != null) { + this.channelSchedule.shutdownNow(); + } + if (this.channelCheckExecutor != null) { + this.channelCheckExecutor.shutdownNow(); + } + } + + public CollectorJobScheduler getCollectorAndJobScheduler() { + return collectorJobScheduler; + } + + public Channel getChannel(final String identity) { + Channel channel = this.clientChannelTable.get(identity); + if (channel == null || !channel.isActive()) { + this.clientChannelTable.remove(identity); + log.error("client {} offline now", identity); + } + return channel; + } + + public void addChannel(final String identity, Channel channel) { + Channel preChannel = this.clientChannelTable.get(identity); + if (preChannel != null && channel.isActive()) { + preChannel.close(); + } + this.clientChannelTable.put(identity, channel); + this.collectorAlertHandler.online(identity); + } + + public void closeChannel(final String identity) { + Channel channel = this.getChannel(identity); + if (channel != null) { + this.collectorJobScheduler.collectorGoOffline(identity); + ClusterMsg.Message message = ClusterMsg.Message.newBuilder().setType(ClusterMsg.MessageType.GO_CLOSE).build(); + this.remotingServer.sendMsg(channel, message); + this.clientChannelTable.remove(identity); + log.info("close collect client success, identity: {}", identity); + } + } + + public boolean isChannelActive(final String identity) { + Channel channel = this.clientChannelTable.get(identity); + return channel != null && channel.isActive(); + } + + public boolean sendMsg(final String identityId, final ClusterMsg.Message message) { + Channel channel = this.getChannel(identityId); + if (channel != null) { + this.remotingServer.sendMsg(channel, message); + return true; + } + return false; + } + + public ClusterMsg.Message sendMsgSync(final String identityId, final ClusterMsg.Message message) { + Channel channel = this.getChannel(identityId); + if (channel != null) { + return this.remotingServer.sendMsgSync(channel, message, 3000); + } + return null; + } + + void dispatchChannelHealthCheck() { + if (channelCheckExecutor == null) { + runChannelHealthCheck(); + return; + } + synchronized (channelCheckLock) { + if (channelCheckRunning) { + channelCheckPending = true; + return; + } + channelCheckRunning = true; + } + submitChannelHealthCheck(); + } + + @Override + public void run(String... args) throws Exception { + this.start(); + } + + /** + * manage netty event listener + */ + public class ManageNettyEventListener implements NettyEventListener { + + @Override + public void onChannelIdle(Channel channel) { + String identity = null; + for (Map.Entry<String, Channel> entry : ManageServer.this.clientChannelTable.entrySet()) { + if (entry.getValue().equals(channel)) { + identity = entry.getKey(); + break; + } + } + if (identity != null) { + ManageServer.this.clientChannelTable.remove(identity); + ManageServer.this.collectorJobScheduler.collectorGoOffline(identity); + channel.close(); + log.info("handle idle event triggered. the client {} is going offline.", identity); + } + } + } + + private ExecutorService createChannelCheckExecutor(VirtualThreadProperties virtualThreadProperties) { + VirtualThreadProperties properties = + virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties; + if (!properties.enabled()) { + return null; + } + return Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name("manager-channel-check-vt-", 0) + .uncaughtExceptionHandler((thread, throwable) -> log.error("Channel checker has uncaughtException.", throwable)) + .factory()); + } + + private void submitChannelHealthCheck() { + boolean submitted = false; + try { + channelCheckExecutor.execute(() -> { + try { + runChannelHealthCheck(); + } finally { + onChannelHealthCheckComplete(); + } + }); + submitted = true; + } finally { + if (!submitted) { + synchronized (channelCheckLock) { + channelCheckRunning = false; + channelCheckPending = false; + } + } + } + } + + private void onChannelHealthCheckComplete() { + boolean shouldRunAgain; + synchronized (channelCheckLock) { + if (channelCheckPending) { + channelCheckPending = false; + shouldRunAgain = true; + } else { + channelCheckRunning = false; + shouldRunAgain = false; + } + } + if (shouldRunAgain) { + submitChannelHealthCheck(); + } + } + + private void runChannelHealthCheck() { + try { + this.clientChannelTable.forEach((collector, channel) -> { + if (!channel.isActive()) { + channel.closeFuture(); + this.clientChannelTable.remove(collector); + this.collectorJobScheduler.collectorGoOffline(collector); + this.collectorAlertHandler.offline(collector); + } + }); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectCyclicDataResponseProcessor.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectCyclicDataResponseProcessor.java new file mode 100644 index 0000000..fd8c828 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectCyclicDataResponseProcessor.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.scheduler.netty.process; + +import io.netty.channel.ChannelHandlerContext; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.common.util.ArrowUtil; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle cyclic data response message + */ +@Slf4j +public class CollectCyclicDataResponseProcessor implements NettyRemotingProcessor { + + private final CommonDataQueue dataQueue; + + public CollectCyclicDataResponseProcessor(CommonDataQueue dataQueue) { + this.dataQueue = dataQueue; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + List<CollectRep.MetricsData> metricsDataList = ArrowUtil.deserializeMetricsData(message.getMsg().toByteArray()); + for (CollectRep.MetricsData metricsData : metricsDataList) { + if (metricsData != null) { + dataQueue.sendMetricsData(metricsData); + } + } + return null; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectCyclicServiceDiscoveryDataResponseProcessor.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectCyclicServiceDiscoveryDataResponseProcessor.java new file mode 100644 index 0000000..17cc39e --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectCyclicServiceDiscoveryDataResponseProcessor.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.scheduler.netty.process; + +import io.netty.channel.ChannelHandlerContext; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.common.util.ArrowUtil; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle cyclic data response message + */ +@Slf4j +public class CollectCyclicServiceDiscoveryDataResponseProcessor implements NettyRemotingProcessor { + + private final CommonDataQueue dataQueue; + + public CollectCyclicServiceDiscoveryDataResponseProcessor(CommonDataQueue dataQueue) { + this.dataQueue = dataQueue; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + List<CollectRep.MetricsData> metricsDataList = ArrowUtil.deserializeMetricsData(message.getMsg().toByteArray()); + for (CollectRep.MetricsData metricsData : metricsDataList) { + if (metricsData != null) { + dataQueue.sendServiceDiscoveryData(metricsData); + } + } + return null; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectOneTimeDataResponseProcessor.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectOneTimeDataResponseProcessor.java new file mode 100644 index 0000000..b9e064d --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectOneTimeDataResponseProcessor.java @@ -0,0 +1,48 @@ +/* + * 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.manager.scheduler.netty.process; + +import io.netty.channel.ChannelHandlerContext; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.ArrowUtil; +import org.apache.hertzbeat.manager.scheduler.netty.ManageServer; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle one-time collect data response message + */ +@Slf4j +public class CollectOneTimeDataResponseProcessor implements NettyRemotingProcessor { + + private final ManageServer manageServer; + + public CollectOneTimeDataResponseProcessor(ManageServer manageServer) { + this.manageServer = manageServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + + List<CollectRep.MetricsData> metricsDataList = ArrowUtil.deserializeMetricsData(message.getMsg().toByteArray()); + this.manageServer.getCollectorAndJobScheduler().collectSyncJobResponse(metricsDataList); + return null; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectorOfflineProcessor.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectorOfflineProcessor.java new file mode 100644 index 0000000..85c5b0d --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectorOfflineProcessor.java @@ -0,0 +1,45 @@ +/* + * 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.manager.scheduler.netty.process; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.manager.scheduler.netty.ManageServer; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle collector offline message + */ +@Slf4j +public class CollectorOfflineProcessor implements NettyRemotingProcessor { + + private final ManageServer manageServer; + + public CollectorOfflineProcessor(final ManageServer manageServer) { + this.manageServer = manageServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + String collector = message.getIdentity(); + log.info("the collector {} actively requests to go offline.", collector); + this.manageServer.getCollectorAndJobScheduler().collectorGoOffline(collector); + return null; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectorOnlineProcessor.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectorOnlineProcessor.java new file mode 100644 index 0000000..9a7e4f4 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/CollectorOnlineProcessor.java @@ -0,0 +1,66 @@ +/* + * 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.manager.scheduler.netty.process; + +import com.google.protobuf.ByteString; +import io.netty.channel.ChannelHandlerContext; +import java.net.InetSocketAddress; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.entity.dto.CollectorInfo; +import org.apache.hertzbeat.common.entity.dto.ServerInfo; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.util.AesUtil; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.scheduler.netty.ManageServer; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle collector online message + */ +@Slf4j +public class CollectorOnlineProcessor implements NettyRemotingProcessor { + private final ManageServer manageServer; + + public CollectorOnlineProcessor(final ManageServer manageServer) { + this.manageServer = manageServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + String collector = message.getIdentity(); + log.info("the collector {} actively requests to go online.", collector); + String msg = message.getMsg().toStringUtf8(); + CollectorInfo collectorInfo = JsonUtil.fromJson(msg, CollectorInfo.class); + if (collectorInfo != null && StringUtils.isBlank(collectorInfo.getIp())) { + // fetch remote ip address + InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress(); + String clientIP = socketAddress.getAddress().getHostAddress(); + collectorInfo.setIp(clientIP); + } + this.manageServer.addChannel(collector, ctx.channel()); + this.manageServer.getCollectorAndJobScheduler().collectorGoOnline(collector, collectorInfo); + ServerInfo serverInfo = ServerInfo.builder().aesSecret(AesUtil.getDefaultSecretKey()).build(); + return ClusterMsg.Message.newBuilder() + .setIdentity(message.getIdentity()) + .setDirection(ClusterMsg.Direction.RESPONSE) + .setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(serverInfo))) + .setType(ClusterMsg.MessageType.GO_ONLINE) + .build(); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/HeartbeatProcessor.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/HeartbeatProcessor.java new file mode 100644 index 0000000..8cf7050 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/scheduler/netty/process/HeartbeatProcessor.java @@ -0,0 +1,59 @@ +/* + * 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.manager.scheduler.netty.process; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.manager.scheduler.netty.ManageServer; +import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle heartbeat message + */ +@Slf4j +public class HeartbeatProcessor implements NettyRemotingProcessor { + + private final ManageServer manageServer; + + public HeartbeatProcessor(final ManageServer manageServer) { + this.manageServer = manageServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + String identity = message.getIdentity(); + boolean isChannelActive = this.manageServer.isChannelActive(identity); + if (!isChannelActive) { + this.manageServer.addChannel(identity, ctx.channel()); + isChannelActive = this.manageServer.isChannelActive(identity); + if (!isChannelActive) { + log.info("the collector {} is not online.", identity); + return null; + } else { + this.manageServer.getCollectorAndJobScheduler().collectorGoOnline(identity, null); + } + } + if (log.isDebugEnabled()) { + log.debug("server receive collector {} heartbeat", message.getIdentity()); + } + return ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.HEARTBEAT) + .build(); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AccountService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AccountService.java new file mode 100644 index 0000000..58fe2c1 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AccountService.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.service; + +import java.util.List; +import java.util.Map; +import javax.naming.AuthenticationException; + +import org.apache.hertzbeat.common.entity.manager.AuthToken; +import org.apache.hertzbeat.manager.pojo.dto.LoginDto; +import org.apache.hertzbeat.manager.pojo.dto.RefreshTokenResponse; + +/** + * Account service + */ +public interface AccountService { + /** + * Account password login to obtain associated user information + * + * @param loginDto loginDto + * @return token info + * @throws AuthenticationException when authentication is failed + */ + Map<String, String> authGetToken(LoginDto loginDto) throws AuthenticationException; + + /** + * Use refresh TOKEN to re-acquire TOKEN + * + * @param refreshToken refreshToken + * @return token and refresh token + * @throws Exception failed to refresh + */ + RefreshTokenResponse refreshToken(String refreshToken) throws Exception; + + /** + * Generate a managed API token and persist it for lifecycle management + * + * @param tokenName optional name/description for the token + * @param expireSeconds optional expiration time in seconds, null means never expire + * @return token string + */ + String generateToken(String tokenName, Long expireSeconds) throws AuthenticationException; + + /** + * List all API tokens + * + * @return list of auth tokens + */ + List<AuthToken> listTokens(); + + /** + * Delete/revoke an API token by id + * + * @param id token id + */ + void deleteToken(Long id) throws AuthenticationException; + + /** + * Check the status of a managed token (revocation + expiration) + * + * @param tokenValue the raw token string + * @return null if the token is valid, otherwise a rejection reason string + */ + String checkTokenStatus(String tokenValue); + + /** + * Check whether the current account behind a managed token is still valid + * and still owns the roles claimed in that token. + * + * @param userId token subject + * @param claimedRoles roles embedded in the token claims + * @return null if the token owner is still valid, otherwise a rejection reason string + */ + String checkManagedTokenAccess(String userId, List<String> claimedRoles); + + /** + * Update the last used time of a managed token + * + * @param tokenValue the raw token string + */ + void touchTokenLastUsedTime(String tokenValue); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AppService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AppService.java new file mode 100644 index 0000000..9aef4f1 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AppService.java @@ -0,0 +1,143 @@ +/* + * 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.manager.service; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.manager.pojo.dto.Hierarchy; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.apache.hertzbeat.manager.pojo.dto.TemplateConfig; + +/** + * Monitoring Type Management Interface + */ +public interface AppService { + + /** + * Query the defined parameter structure based on the monitoring type + * @param app Monitoring type + * @return list of parameter structures + */ + List<ParamDefineInfo> getAppParamDefines(String app); + + Job getPushDefine(Long monitorId); + + /** + * get auto generate dynamic template define + * for prometheus and more type + * @param monitorId monitor id + * @return define + */ + Job getAutoGenerateDynamicDefine(Long monitorId); + + /** + * Get monitor structure definition based on monitor type name + * + * @param app Monitoring type name + * @return Monitoring Structure Definition + * @throws IllegalArgumentException Thrown when there is no monitoring type with the corresponding name that is not supported + */ + Job getAppDefine(String app) throws IllegalArgumentException; + + /** + * Get monitor structure definition based on monitor type name + * @param app Monitoring type name + * @return Monitoring Structure Definition Optional + */ + Optional<Job> getAppDefineOption(String app); + + /** + * Get the defined metrics of the app + * + * @param app monitoring type + * @return target + */ + List<String> getAppDefineMetricNames(String app); + + + /** + * Get defined monitoring I 18 N resources + * + * @param lang Language type + * @return I18N Resources + */ + Map<String, String> getI18nResources(String lang); + + /** + * Get the I 18 N resources of the monitoring type + * + * @param lang Language type + * @return I18N Resources + */ + Map<String, String> getI18nApps(String lang); + + /** + * Query all types of monitoring hierarchy + * + * @param lang language + * @return hierarchy information + */ + List<Hierarchy> getAllAppHierarchy(String lang); + + /** + * Get the monitoring hierarchy based on the monitoring type + * + * @param app monitoring type + * @param lang language + * @return hierarchy information + */ + List<Hierarchy> getAppHierarchy(String app, String lang); + + /** + * Get all app define + * + * @return defines + */ + Map<String, Job> getAllAppDefines(); + + /** + * app define file content str + * + * @param app app + * @return file content + */ + String getMonitorDefineFileContent(String app); + + /** + * update and apply app define yml + * + * @param ymlContent yml content + * @param isModify is modified? + */ + void applyMonitorDefineYml(String ymlContent, boolean isModify); + + /** + * delete monitor define yml + * + * @param app app + */ + void deleteMonitorDefine(String app); + + /** + * update custom template config in memory + * @param config template config + */ + void updateCustomTemplateConfig(TemplateConfig config); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/BulletinService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/BulletinService.java new file mode 100644 index 0000000..f69840e --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/BulletinService.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.service; + +import java.util.List; +import java.util.Optional; +import org.apache.hertzbeat.common.entity.manager.Bulletin; +import org.apache.hertzbeat.manager.pojo.dto.BulletinMetricsData; +import org.springframework.data.domain.Page; + +/** + * Bulletin Service + */ +public interface BulletinService { + + /** + * validate Bulletin + */ + void validate(Bulletin bulletin) throws IllegalArgumentException; + + /** + * Get Bulletin by id + */ + Optional<Bulletin> getBulletinById(Long id); + + /** + * Save Bulletin + */ + void editBulletin(Bulletin bulletin); + + /** + * Add Bulletin + */ + void addBulletin(Bulletin bulletin); + + /** + * deal with the bulletin + */ + BulletinMetricsData buildBulletinMetricsData(Long id); + + /** + * query bulletins + * @param search search name + * @param pageIndex page index + * @param pageSize page size + * @return bulletins + */ + Page<Bulletin> getBulletins(String search, Integer pageIndex, Integer pageSize); + + /** + * delete bulletins + * @param ids bulletin ids + */ + void deleteBulletins(List<Long> ids); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/CollectorService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/CollectorService.java new file mode 100644 index 0000000..f5ef2fc --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/CollectorService.java @@ -0,0 +1,71 @@ +/* + * 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.manager.service; + +import java.util.List; +import java.util.Map; +import org.apache.hertzbeat.manager.pojo.dto.CollectorSummary; +import org.springframework.data.domain.Page; + +/** + * collector service + */ +public interface CollectorService { + + /** + * Dynamic conditional query + * @param name Collector Name + * @param pageIndex current pageIndex + * @param pageSize Number of list pagination + * + * @return Search result + */ + Page<CollectorSummary> getCollectors(String name, int pageIndex, Integer pageSize); + + /** + * delete registered collectors + * @param collectors collector + */ + void deleteRegisteredCollector(List<String> collectors); + + /** + * is has the collector name + * @param collector collector name + * @return return true if it has + */ + boolean hasCollector(String collector); + + /** + * Generate Collector Deploy Info + * @param collector collector name + */ + Map<String, String> generateCollectorDeployInfo(String collector); + + /** + * Makes Collectors Offline + * @param collectors collector names + */ + void makeCollectorsOffline(List<String> collectors); + + /** + * Makes Collectors Online + * @param collectors collector names + */ + void makeCollectorsOnline(List<String> collectors); + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/ConfigService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/ConfigService.java new file mode 100644 index 0000000..418c7c5 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/ConfigService.java @@ -0,0 +1,47 @@ +/* + * 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.manager.service; + +import org.apache.hertzbeat.manager.pojo.dto.TemplateConfig; + +/** + * Provides operations for the GeneralConfigService + */ +public interface ConfigService { + + /** + * save config + * @param type config type + * @param config need save configuration + */ + void saveConfig(String type, Object config); + + /** + * get config + * @param type config type + * @return config + */ + Object getConfig(String type); + + /** + * Update the app template config + * @param app monitoring type + * @param template template config + */ + void updateTemplateAppConfig(String app, TemplateConfig.AppTemplate template); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/ImExportService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/ImExportService.java new file mode 100644 index 0000000..91bbd5f --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/ImExportService.java @@ -0,0 +1,56 @@ +/* + * 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.manager.service; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +/** + * Configuration Import Export + * Created by gcdd1993 on 2023/3/31 + */ +public interface ImExportService { + + /** + * Import Configuration + * @param taskName task name + * @param is input stream + */ + void importConfig(String taskName, InputStream is); + + /** + * Export Configuration + * @param os output stream + * @param configList configuration list + */ + void exportConfig(OutputStream os, List<Long> configList); + + /** + * Export file type + * @return file type + */ + String type(); + + /** + * Get Export File Name + * @return file name + */ + String getFileName(); + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MetricsFavoriteService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MetricsFavoriteService.java new file mode 100644 index 0000000..8971cd3 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MetricsFavoriteService.java @@ -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.manager.service; + +import java.util.Set; + +/** + * Metrics Favorite Service + */ +public interface MetricsFavoriteService { + + /** + * Add metrics to favorites + * + * @param creator user id + * @param monitorId monitor id + * @param metricsName metrics name + */ + void addMetricsFavorite(String creator, Long monitorId, String metricsName); + + /** + * Remove metrics from favorites + * + * @param userId user id + * @param monitorId monitor id + * @param metricsName metrics name + */ + void removeMetricsFavorite(String userId, Long monitorId, String metricsName); + + /** + * Get user's favorited metrics names for a specific monitor + * + * @param userId user id + * @param monitorId monitor id + * @return set of favorited metrics names + */ + Set<String> getUserFavoritedMetrics(String userId, Long monitorId); + + /** + * Remove metrics from monitor ids + * + * @param monitorIds monitor ids + */ + void deleteFavoritesByMonitorIdIn(Set<Long> monitorIds); +} \ No newline at end of file diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MonitorService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MonitorService.java new file mode 100644 index 0000000..083d6e1 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MonitorService.java @@ -0,0 +1,206 @@ +/* + * 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.manager.service; + +import jakarta.servlet.http.HttpServletResponse; +import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.apache.hertzbeat.manager.pojo.dto.AppCount; +import org.apache.hertzbeat.manager.pojo.dto.MonitorDto; +import org.apache.hertzbeat.manager.support.exception.MonitorDetectException; +import org.springframework.data.domain.Page; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Set; + +/** + * Monitoring and management services + */ +public interface MonitorService { + + /** + * Monitoring Availability Probes + * + * @param monitor Monitoring entity information + * @param params Parameter information + * @param collector collector pinned + * @throws MonitorDetectException Probe failure throws + */ + void detectMonitor(Monitor monitor, List<Param> params, String collector) throws MonitorDetectException; + + /** + * Add monitoring + * + * @param monitor Monitoring Entity + * @param params Parameter information + * @param collector collector pinned + * @param dashboard grafana dashboard + * @throws RuntimeException Add process exception throw + */ + void addMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard dashboard) throws RuntimeException; + + /** + * Verify the correctness of request data parameters + * + * @param monitorDto monitorDto + * @param isModify Whether it is a modification monitoring + * @throws IllegalArgumentException Validation parameter error thrown + */ + void validate(MonitorDto monitorDto, Boolean isModify) throws IllegalArgumentException; + + /** + * Modify update monitoring + * + * @param monitor Monitor Entity + * @param params Parameter information + * @param collector collector pinned + * @param dashboard grafana dashboard + * @throws RuntimeException Exception thrown during modification + */ + void modifyMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard dashboard) throws RuntimeException; + + /** + * Delete Monitor + * + * @param id Monitor ID + * @throws RuntimeException Exception thrown during deletion + */ + void deleteMonitor(long id) throws RuntimeException; + + /** + * Batch delete monitoring + * + * @param ids Monitoring ID List + * @throws RuntimeException Exception thrown during deletion + */ + void deleteMonitors(Set<Long> ids) throws RuntimeException; + + /** + * Get monitoring information + * + * @param id Monitor ID + * @return MonitorDto Monitor Entity + * @throws RuntimeException Exception thrown during query + */ + MonitorDto getMonitorDto(long id) throws RuntimeException; + + /** + * Dynamic conditional query + * + * @param monitorIds Monitor ID List + * @param app Monitor Type + * @param search Monitor Host support fuzzy query + * @param status Monitor Status 0:no monitor,1:usable,2:disabled,9:all status + * @param sort Sort Field + * @param order Sort mode eg:asc desc + * @param pageIndex List current page + * @param pageSize Number of list pagination + * @param labels Monitor labels + * @return Search Result + */ + Page<Monitor> getMonitors(List<Long> monitorIds, String app, String search, Byte status, String sort, String order, int pageIndex, int pageSize, String labels); + + /** + * Unmanaged monitoring items in batches according to the monitoring ID list + * + * @param ids Monitoring ID List + */ + void cancelManageMonitors(Set<Long> ids); + + /** + * Start the managed monitoring items in batches according to the monitoring ID list + * + * @param ids Monitoring ID List + */ + void enableManageMonitors(Set<Long> ids); + + /** + * Query the monitoring category and its corresponding monitoring quantity + * + * @return Monitoring Category and Monitoring Quantity Mapping + */ + List<AppCount> getAllAppMonitorsCount(); + + /** + * Query monitoring + * + * @param monitorId Monitor ID + * @return Monitor information + */ + Monitor getMonitor(Long monitorId); + + /** + * Update the status of the specified monitor + * + * @param monitorId monitorId + * @param status monitor status + */ + void updateMonitorStatus(Long monitorId, byte status); + + /** + * Query the list of all monitoring information under the specified monitoring type + * + * @param app Monitor Type + * @return Monitor Entity List + */ + List<Monitor> getAppMonitors(String app); + + /** + * Export Monitoring Configuration + * + * @param ids monitor id list + * @param type file type + * @param res response + * @throws Exception This exception will be thrown if the export fails + */ + void export(List<Long> ids, String type, HttpServletResponse res) throws Exception; + + /** + * Export All Monitoring Configuration + * + * @param type file type + * @param res response + * @throws Exception This exception will be thrown if the export fails + */ + void exportAll(String type, HttpServletResponse res) throws Exception; + + /** + * Import Monitoring Configuration + * + * @param file configuration file + * @throws Exception This exception will be thrown if the export fails + */ + void importConfig(MultipartFile file) throws Exception; + + /** + * update app collect job by app + * + * @param job job content + */ + void updateAppCollectJob(Job job); + + /** + * Copy monitor by id + * + * @param id Monitor id + */ + void copyMonitor(Long id); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/ObjectStoreService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/ObjectStoreService.java new file mode 100644 index 0000000..0301764 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/ObjectStoreService.java @@ -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.manager.service; + +import java.io.InputStream; +import java.util.List; +import org.apache.hertzbeat.manager.pojo.dto.FileDTO; +import org.apache.hertzbeat.manager.pojo.dto.ObjectStoreDTO; + +/** + * File storage service + */ +public interface ObjectStoreService { + + /** + * save file + * @param filePath file path,eg:hertzbeat/111.json + * @param is input stream + */ + boolean upload(String filePath, InputStream is); + + /** + * remove file + * @param filePath file path,eg:hertzbeat/111.json + */ + void remove(String filePath); + + /** + * whether the file exists + * @param filePath file path,eg:hertzbeat/111.json + */ + boolean isExist(String filePath); + + /** + * read file + * @param filePath file path,eg:hertzbeat/111.json + * @return file + */ + FileDTO download(String filePath); + + /** + * Enumeration file + * @param dir file catalog + * @return file list + */ + List<FileDTO> list(String dir); + + ObjectStoreDTO.Type type(); + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/PluginService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/PluginService.java new file mode 100644 index 0000000..e7ea483 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/PluginService.java @@ -0,0 +1,100 @@ +/* + * 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.manager.service; + +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.apache.hertzbeat.common.entity.manager.PluginMetadata; +import org.apache.hertzbeat.common.entity.plugin.PluginContext; +import org.apache.hertzbeat.manager.pojo.dto.PluginUpload; +import org.apache.hertzbeat.manager.pojo.dto.PluginParam; +import org.apache.hertzbeat.manager.pojo.dto.PluginParametersVO; +import org.springframework.data.domain.Page; + +/** + * plugin service + */ +public interface PluginService { + + /** + * save plugin + */ + void savePlugin(PluginUpload pluginUpload); + + /** + * Determine whether the plugin is enabled + * + * @param clazz plugin Class + * @return return true if enabled + */ + boolean pluginIsEnable(Class<?> clazz); + + + /** + * get plugin page list + * + * @param search plugin name search + * @param pageIndex List current page + * @param pageSize Number of list pagination + * @return Plugins + */ + Page<PluginMetadata> getPlugins(String search, int pageIndex, int pageSize); + + /** + * execute plugin + * @param clazz plugin interface + * @param execute run plugin logic + * @param <T> plugin type + */ + <T> void pluginExecute(Class<T> clazz, Consumer<T> execute); + + + /** + * execute plugin + * + * @param clazz plugin interface + * @param execute run plugin logic + * @param <T> plugin type + */ + <T> void pluginExecute(Class<T> clazz, BiConsumer<T, PluginContext> execute); + + + /** + * delete plugin + * + * @param ids set of plugin id + */ + void deletePlugins(Set<Long> ids); + + void updateStatus(PluginMetadata plugin); + + /** + * get param define + * @param pluginMetadataId plugin id + */ + PluginParametersVO getParamDefine(Long pluginMetadataId); + + /** + * save plugin param + * @param params params + */ + void savePluginParam(List<PluginParam> params); + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/StatusPageService.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/StatusPageService.java new file mode 100644 index 0000000..19a64b5 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/StatusPageService.java @@ -0,0 +1,138 @@ +/* + * 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.manager.service; + +import java.util.List; + +import org.apache.hertzbeat.manager.pojo.dto.ComponentStatus; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageComponentInfo; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageIncidentInfo; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageOrgInfo; +import org.springframework.data.domain.Page; + +/** + * status page service interface. + */ +public interface StatusPageService { + + /** + * query status page organization. + * + * @return status page organization + */ + StatusPageOrgInfo queryStatusPageOrg(); + + /** + * save status page organization. + * + * @return status page organization + */ + StatusPageOrgInfo saveStatusPageOrg(StatusPageOrgInfo statusPageOrg); + + /** + * query status page components. + * + * @return status page components + */ + List<StatusPageComponentInfo> queryStatusPageComponents(); + + /** + * save status page component. + * + * @param statusPageComponent status page component + */ + void newStatusPageComponent(StatusPageComponentInfo statusPageComponent); + + /** + * update status page component. + * + * @param statusPageComponent status page component + */ + void updateStatusPageComponent(StatusPageComponentInfo statusPageComponent); + + /** + * delete status page component. + * + * @param id status page component id + */ + void deleteStatusPageComponent(long id); + + /** + * query status page component. + * + * @param id status page component id + * @return status page component + */ + StatusPageComponentInfo queryStatusPageComponent(long id); + + /** + * query status page components status. + * + * @return status page components status + */ + List<ComponentStatus> queryComponentsStatus(); + + /** + * query status page component status. + * + * @param id status page component id + * @return status page component status + */ + ComponentStatus queryComponentStatus(long id); + + /** + * query status page incidents. + * + * @param search Search Target + * @param startTime Start Time + * @param endTime End Time + * @param pageIndex List current page + * @param pageSize Number of list pages + * @return The query results + */ + Page<StatusPageIncidentInfo> queryStatusPageIncidents(String search, Long startTime, Long endTime, int pageIndex, int pageSize); + + /** + * query status page incident. + * + * @param id status page incident id + * @return status page incident + */ + StatusPageIncidentInfo queryStatusPageIncident(long id); + + /** + * save status page incident. + * + * @param statusPageIncident status page incident + */ + void newStatusPageIncident(StatusPageIncidentInfo statusPageIncident); + + /** + * update status page incident. + * + * @param statusPageIncident status page incident + */ + void updateStatusPageIncident(StatusPageIncidentInfo statusPageIncident); + + /** + * delete status page incident. + * + * @param id status page incident id + */ + void deleteStatusPageIncident(long id); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/helper/MonitorImExportHelper.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/helper/MonitorImExportHelper.java new file mode 100644 index 0000000..9585feb --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/helper/MonitorImExportHelper.java @@ -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.manager.service.helper; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.ExportFileConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.common.util.FileUtil; +import org.apache.hertzbeat.manager.config.ManagerSseManager; +import org.apache.hertzbeat.manager.service.ImExportService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Helper class for monitor import and export operations + */ +@Component +@Slf4j +public class MonitorImExportHelper { + + private static final String CONTENT_VALUE = MediaType.APPLICATION_OCTET_STREAM_VALUE + SignConstants.SINGLE_MARK + + "charset=" + StandardCharsets.UTF_8; + + private final Map<String, ImExportService> imExportServiceMap = new HashMap<>(); + private final ManagerSseManager managerSseManager; + + public MonitorImExportHelper(List<ImExportService> imExportServiceList, ManagerSseManager managerSseManager) { + imExportServiceList.forEach(it -> imExportServiceMap.put(it.type(), it)); + this.managerSseManager = managerSseManager; + } + + public void export(List<Long> ids, String type, HttpServletResponse res) throws Exception { + var imExportService = imExportServiceMap.get(type); + if (imExportService == null) { + throw new IllegalArgumentException("not support export type: " + type); + } + var fileName = imExportService.getFileName(); + res.setHeader(HttpHeaders.CONTENT_DISPOSITION, CONTENT_VALUE); + res.setContentType(CONTENT_VALUE); + res.setHeader(HttpHeaders.CONTENT_DISPOSITION, + "attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)); + res.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION); + imExportService.exportConfig(res.getOutputStream(), ids); + } + + public void importConfig(MultipartFile file) throws Exception { + var fileName = FileUtil.getFileName(file); + var type = FileUtil.getFileType(file); + try { + if (!imExportServiceMap.containsKey(type)) { + String errMsg = ExportFileConstants.FILE + " " + fileName + " is not supported."; + throw new RuntimeException(errMsg); + } + var imExportService = imExportServiceMap.get(type); + imExportService.importConfig(fileName, file.getInputStream()); + } catch (Exception e) { + managerSseManager.broadcastImportTaskFail(fileName, e.getMessage()); + throw e; + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractGeneralConfigServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractGeneralConfigServiceImpl.java new file mode 100644 index 0000000..41b649d --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractGeneralConfigServiceImpl.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.base.service.GeneralConfigService; +import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.transaction.annotation.Transactional; +import tools.jackson.core.type.TypeReference; + +/** + * <p>Abstract implementation of GeneralConfigService, providing CRUD operations for configurations.</p> + */ +@Slf4j +abstract class AbstractGeneralConfigServiceImpl<T> implements GeneralConfigService<T> { + + protected final GeneralConfigDao generalConfigDao; + + protected AbstractGeneralConfigServiceImpl(GeneralConfigDao generalConfigDao) { + this.generalConfigDao = generalConfigDao; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveConfig(T config) { + String contentJson = JsonUtil.toJson(config); + if (contentJson == null) { + log.error("Failed to serialize configuration for type {}", type()); + throw new RuntimeException("Save config failed: serialization error"); + } + GeneralConfig generalConfig2Save = GeneralConfig.builder() + .type(type()) + .content(contentJson) + .build(); + generalConfigDao.save(generalConfig2Save); + log.info("Configuration of type {} saved successfully", type()); + handler(getConfig()); + } + + @Override + public T getConfig() { + GeneralConfig generalConfig = generalConfigDao.findByType(type()); + if (generalConfig == null) { + return null; + } + return JsonUtil.fromJson(generalConfig.getContent(), getTypeReference()); + } + + protected abstract TypeReference<T> getTypeReference(); +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java new file mode 100644 index 0000000..901c513 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java @@ -0,0 +1,243 @@ +/* + * 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.manager.service.impl; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import cn.afterturn.easypoi.excel.annotation.ExcelCollection; +import cn.afterturn.easypoi.excel.annotation.ExcelEntity; +import cn.afterturn.easypoi.excel.annotation.ExcelTarget; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.annotation.Resource; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.ImExportTaskConstant; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.apache.hertzbeat.manager.config.ManagerSseManager; +import org.apache.hertzbeat.manager.pojo.dto.MonitorDto; +import org.apache.hertzbeat.manager.service.ImExportService; +import org.apache.hertzbeat.manager.service.MonitorService; +import org.springframework.beans.BeanUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.util.CollectionUtils; + +import java.io.InputStream; +import java.io.OutputStream; +import java.time.LocalDate; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * class AbstractImExportServiceImpl + */ +@Slf4j +public abstract class AbstractImExportServiceImpl implements ImExportService { + + @Resource + @Lazy + private MonitorService monitorService; + + @Resource + private ManagerSseManager managerSseManager; + + @Override + public void importConfig(String taskName, InputStream is) { + var formList = parseImport(is).stream().map(this::convert).toList(); + if (!CollectionUtils.isEmpty(formList)) { + int totalElements = formList.size(); + validateImportBatch(formList); + int progressInterval = Math.max(1, totalElements / 10); + for (int i = 0; i < totalElements; i++) { + MonitorDto monitorDto = formList.get(i); + monitorService.addMonitor(monitorDto.getMonitor(), monitorDto.getParams(), monitorDto.getCollector(), monitorDto.getGrafanaDashboard()); + if (totalElements >= ImExportTaskConstant.IMPORT_TASK_PROCESS_THRESHOLD && ((i + 1) % progressInterval == 0) && (i + 1 < totalElements)) { + managerSseManager.broadcastImportTaskInProgress(taskName, (int) ((i + 1) * 100.0 / totalElements)); + } + } + managerSseManager.broadcastImportTaskSuccess(taskName); + } + } + + /** + * Validate every entry of an import batch before any monitor is persisted. + * Beyond the per-entry checks (params/collector/name uniqueness against the DB), this also detects + * duplicate monitor names within the import file itself, which the single-entry validation cannot + * catch because nothing has been persisted yet when the conflicting rows are validated. + * + * @param formList parsed monitors to import + * @throws IllegalArgumentException if any entry is invalid or names collide within the file + */ + private void validateImportBatch(List<MonitorDto> formList) { + Set<String> names = new HashSet<>(formList.size()); + for (MonitorDto monitorDto : formList) { + monitorService.validate(monitorDto, false); + String name = monitorDto.getMonitorInfo().getName(); + if (!names.add(name)) { + throw new IllegalArgumentException("Duplicate monitor name in import file: " + name); + } + } + } + + @Override + public void exportConfig(OutputStream os, List<Long> configList) { + var monitorList = configList.stream().map(it -> monitorService.getMonitorDto(it)).filter(Objects::nonNull).map(this::convert).toList(); + writeOs(monitorList, os); + } + + /** + * Parsing an input stream into a form + * + * @param is input stream + * @return form + */ + abstract List<ExportMonitorDTO> parseImport(InputStream is); + + /** + * Export Configuration to Output Stream + * + * @param monitorList config list + * @param os output stream + */ + abstract void writeOs(List<ExportMonitorDTO> monitorList, OutputStream os); + + private ExportMonitorDTO convert(MonitorDto dto) { + var exportMonitor = new ExportMonitorDTO(); + var monitor = new MonitorDTO(); + BeanUtils.copyProperties(dto.getMonitor(), monitor); + exportMonitor.setMonitor(monitor); + exportMonitor.setParams(dto.getParams().stream().map(it -> { + var param = new ParamDTO(); + param.setField(it.getField()); + param.setType(it.getType()); + param.setValue(it.getParamValue()); + return param; + }).toList()); + exportMonitor.getMonitor().setCollector(dto.getCollector()); + return exportMonitor; + } + + private MonitorDto convert(ExportMonitorDTO exportMonitor) { + if (exportMonitor == null || exportMonitor.monitor == null) { + throw new IllegalArgumentException("exportMonitor and exportMonitor.monitor must not be null"); + } + + var monitorDto = new MonitorDto(); + var monitor = new Monitor(); + log.debug("exportMonitor.monitor{}", exportMonitor.monitor); + if (exportMonitor.monitor != null) { + // Add one more null check + BeanUtils.copyProperties(exportMonitor.monitor, monitor); + if (exportMonitor.params != null) { + String host = exportMonitor.params.stream() + .filter(p -> "host".equals(p.field)).findFirst() + .map(p -> p.value).orElse(null); + String port = exportMonitor.params.stream() + .filter(p -> "port".equals(p.field)).findFirst() + .map(p -> p.value).orElse(null); + if (host != null) { + monitor.setInstance(port != null ? host + ":" + port : host); + } + } + } + monitorDto.setMonitor(monitor); + if (exportMonitor.getMonitor() != null) { + monitorDto.setCollector(exportMonitor.getMonitor().getCollector()); + } + if (exportMonitor.params != null) { + monitorDto.setParams(exportMonitor.params.stream().map(it -> { + var param = new Param(); + param.setField(it.field); + param.setType(it.type); + param.setParamValue(it.value); + return param; + }).toList()); + } else { + monitorDto.setParams(Collections.emptyList()); + } + return monitorDto; + } + + protected String fileNamePrefix() { + return "hertzbeat_monitor_" + LocalDate.now(); + } + + /** + * Export Monitor DTO + */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + @ExcelTarget(value = "ExportMonitorDTO") + public static class ExportMonitorDTO { + @ExcelEntity(name = "Monitor") + private MonitorDTO monitor; + @ExcelCollection(name = "Params") + private List<ParamDTO> params; + } + + /** + * Monitor DTO + */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + @ExcelTarget(value = "MonitorDTO") + public static class MonitorDTO { + @Excel(name = "Name") + private String name; + @Excel(name = "App") + private String app; + @Excel(name = "Host") + private String host; + @Excel(name = "Intervals") + private Integer intervals; + @Excel(name = "Status") + private Byte status; + @Excel(name = "Description") + private String description; + @Excel(name = "Labels") + private Map<String, String> labels; + @Excel(name = "Annotations") + private Map<String, String> annotations; + @Excel(name = "ScheduleType") + private String scheduleType; + @Excel(name = "CronExpression") + private String cronExpression; + @Excel(name = "Collector") + private String collector; + } + + /** + * Param DTO + */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + @ExcelTarget(value = "ParamDTO") + public static class ParamDTO { + @Excel(name = "Field") + private String field; + @Excel(name = "Type") + private Byte type; + @Excel(name = "Value") + private String value; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AccountServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AccountServiceImpl.java new file mode 100644 index 0000000..1cadceb --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AccountServiceImpl.java @@ -0,0 +1,314 @@ +/* + * 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.manager.service.impl; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.usthe.sureness.provider.SurenessAccount; +import com.usthe.sureness.provider.SurenessAccountProvider; +import com.usthe.sureness.provider.ducument.DocumentAccountProvider; +import com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.JsonWebTokenUtil; +import com.usthe.sureness.util.Md5Util; +import com.usthe.sureness.util.SurenessContextHolder; +import io.jsonwebtoken.Claims; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.util.CryptoUtils; +import org.apache.hertzbeat.common.entity.manager.AuthToken; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.dao.AuthTokenDao; +import org.apache.hertzbeat.manager.pojo.dto.LoginDto; +import org.apache.hertzbeat.manager.pojo.dto.RefreshTokenResponse; +import org.apache.hertzbeat.manager.service.AccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; + +import javax.naming.AuthenticationException; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * Implementation of Account service + */ +@Service +@Order(value = Ordered.HIGHEST_PRECEDENCE) +@Slf4j +public class AccountServiceImpl implements AccountService { + + private static final String REFRESH_CLAIM = "refresh"; + + /** + * Token validity time in seconds + */ + private static final long PERIOD_TIME = 3600L; + + /** + * Non-expiring managed token duration: 100 years in seconds + */ + private static final long NON_EXPIRING_TOKEN_SECONDS = 365L * 24 * 3600 * 100; + + private static final byte TOKEN_STATUS_ACTIVE = 0; + + /** + * Custom JWT claim key to mark tokens as managed (persisted in DB for lifecycle management). + * Only tokens with this claim will be validated against the database. + * Legacy tokens without this claim are allowed to pass through for backward compatibility. + */ + public static final String CLAIM_MANAGED = "managed"; + + /** + * Minimum interval (in minutes) between lastUsedTime DB updates for the same token. + * Reduces write pressure under high-frequency requests. + */ + private static final long LAST_USED_UPDATE_INTERVAL_MINUTES = 5; + + /** + * Local cache: tokenHash -> isActive. + * TTL 60s to reduce DB queries; invalidated on token revocation. + */ + private final Cache<String, Boolean> tokenStatusCache = Caffeine.newBuilder() + .maximumSize(1000) + .expireAfterWrite(60, TimeUnit.SECONDS) + .build(); + + /** + * Tracks when each token's lastUsedTime was last written to DB. + * Used to throttle DB writes to at most once per LAST_USED_UPDATE_INTERVAL_MINUTES. + */ + private final ConcurrentHashMap<String, LocalDateTime> lastUsedWriteTimestamps = new ConcurrentHashMap<>(); + + /** + * account data provider + */ + private final SurenessAccountProvider accountProvider; + + public AccountServiceImpl() { + this(new DocumentAccountProvider()); + } + + public AccountServiceImpl(SurenessAccountProvider accountProvider) { + this.accountProvider = accountProvider; + } + + @Autowired + private AuthTokenDao authTokenDao; + + @Override + public Map<String, String> authGetToken(LoginDto loginDto) throws AuthenticationException { + SurenessAccount account = accountProvider.loadAccount(loginDto.getIdentifier()); + if (account == null || StringUtils.isBlank(account.getPassword())) { + throw new AuthenticationException("Incorrect Account or Password"); + } else { + String password = loginDto.getCredential(); + if (StringUtils.isNotBlank(account.getSalt())) { + password = Md5Util.md5(password + account.getSalt()); + } + if (!account.getPassword().equals(password)) { + throw new AuthenticationException("Incorrect Account or Password"); + } + if (account.isDisabledAccount() || account.isExcessiveAttempts()) { + throw new AuthenticationException("Expired or Illegal Account"); + } + } + // Get the roles the user has - rbac + List<String> roles = account.getOwnRoles(); + // Issue TOKEN + String issueToken = issueAccessToken(loginDto.getIdentifier(), roles, PERIOD_TIME); + String issueRefresh = issueRefreshToken(loginDto.getIdentifier(), PERIOD_TIME << 5); + Map<String, String> resp = new HashMap<>(2); + resp.put("token", issueToken); + resp.put("refreshToken", issueRefresh); + resp.put("role", JsonUtil.toJson(roles)); + + return resp; + } + + @Override + public RefreshTokenResponse refreshToken(String refreshToken) throws Exception { + Claims claims = JsonWebTokenUtil.parseJwt(refreshToken); + String userId = claims.getSubject(); + Boolean isRefresh = claims.get(REFRESH_CLAIM, Boolean.class); + if (StringUtils.isBlank(userId) || !Boolean.TRUE.equals(isRefresh)) { + throw new AuthenticationException("Illegal Refresh Token"); + } + SurenessAccount account = accountProvider.loadAccount(userId); + if (account == null) { + throw new AuthenticationException("Not Exists This Token Mapping Account"); + } + if (account.isDisabledAccount() || account.isExcessiveAttempts()) { + throw new AuthenticationException("Expired or Illegal Account"); + } + List<String> roles = account.getOwnRoles(); + String issueToken = issueAccessToken(userId, roles, PERIOD_TIME); + String issueRefresh = issueRefreshToken(userId, PERIOD_TIME << 5); + return new RefreshTokenResponse(issueToken, issueRefresh); + } + + @Override + public String generateToken(String tokenName, Long expireSeconds) throws AuthenticationException { + SubjectSum subjectSum = requireCurrentSubject(); + String userId = getCurrentUserId(subjectSum); + SurenessAccount account = accountProvider.loadAccount(userId); + if (account == null) { + throw new AuthenticationException("Not Exists This Token Mapping Account"); + } + if (account.isDisabledAccount() || account.isExcessiveAttempts()) { + throw new AuthenticationException("Expired or Illegal Account"); + } + List<String> roles = account.getOwnRoles(); + String token = issueApiToken(userId, roles, expireSeconds); + + // Persist token metadata for management + String tokenHash = CryptoUtils.sha256Hex(token); + String tokenMask = maskToken(token); + LocalDateTime expireTime = null; + if (expireSeconds != null && expireSeconds > 0) { + expireTime = LocalDateTime.now().plusSeconds(expireSeconds); + } + AuthToken authToken = AuthToken.builder() + .name(tokenName) + .tokenHash(tokenHash) + .tokenMask(tokenMask) + .status(TOKEN_STATUS_ACTIVE) + .creator(userId) + .expireTime(expireTime) + .build(); + authTokenDao.save(authToken); + + return token; + } + + private String issueAccessToken(String userId, List<String> roles, long expirationSeconds) { + return JsonWebTokenUtil.issueJwt(userId, expirationSeconds, roles, new HashMap<>(0)); + } + + @Override + public List<AuthToken> listTokens() { + SubjectSum subjectSum = SurenessContextHolder.getBindSubject(); + if (subjectSum == null) { + return List.of(); + } + if (subjectSum.hasRole("admin")) { + return authTokenDao.findByStatus(TOKEN_STATUS_ACTIVE); + } + return authTokenDao.findByStatusAndCreator(TOKEN_STATUS_ACTIVE, getCurrentUserId(subjectSum)); + } + + @Override + public void deleteToken(Long id) throws AuthenticationException { + SubjectSum subjectSum = requireCurrentSubject(); + String userId = getCurrentUserId(subjectSum); + AuthToken token = authTokenDao.findById(id).orElse(null); + if (token == null) { + return; + } + if (!subjectSum.hasRole("admin") && !StringUtils.equals(userId, token.getCreator())) { + throw new AuthenticationException("No permission"); + } + tokenStatusCache.invalidate(token.getTokenHash()); + lastUsedWriteTimestamps.remove(token.getTokenHash()); + authTokenDao.deleteById(id); + } + + @Override + public String checkTokenStatus(String tokenValue) { + String tokenHash = CryptoUtils.sha256Hex(tokenValue); + Boolean active = tokenStatusCache.get(tokenHash, + hash -> authTokenDao.existsByTokenHashAndStatus(hash, TOKEN_STATUS_ACTIVE)); + if (!Boolean.TRUE.equals(active)) { + return "Token has been revoked"; + } + return null; + } + + @Override + public String checkManagedTokenAccess(String userId, List<String> claimedRoles) { + if (StringUtils.isBlank(userId)) { + return "Token owner account is no longer valid"; + } + SurenessAccount account = accountProvider.loadAccount(userId); + if (account == null || account.isDisabledAccount() || account.isExcessiveAttempts()) { + return "Token owner account is no longer valid"; + } + if (claimedRoles == null || claimedRoles.isEmpty()) { + return null; + } + List<String> currentRoles = account.getOwnRoles(); + if (currentRoles == null || !new HashSet<>(currentRoles).containsAll(claimedRoles)) { + return "Token permissions are outdated"; + } + return null; + } + + @Override + public void touchTokenLastUsedTime(String tokenValue) { + String tokenHash = CryptoUtils.sha256Hex(tokenValue); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime lastWrite = lastUsedWriteTimestamps.get(tokenHash); + if (lastWrite != null && lastWrite.plusMinutes(LAST_USED_UPDATE_INTERVAL_MINUTES).isAfter(now)) { + return; + } + lastUsedWriteTimestamps.put(tokenHash, now); + authTokenDao.updateLastUsedTime(tokenHash, now); + } + + /** + * Issue a api token with the "managed" claim. + * This claim distinguishes new managed tokens from legacy unmanaged tokens, + * allowing the validation filter to only check managed tokens against the DB. + * + * @param expireSeconds optional expiration in seconds, null means never expire + */ + private String issueApiToken(String userId, List<String> roles, Long expireSeconds) { + Map<String, Object> customClaimMap = new HashMap<>(2); + customClaimMap.put(CLAIM_MANAGED, true); + long effectiveExpire = (expireSeconds != null && expireSeconds > 0) ? expireSeconds : NON_EXPIRING_TOKEN_SECONDS; + return JsonWebTokenUtil.issueJwt(userId, effectiveExpire, roles, customClaimMap); + } + + private String issueRefreshToken(String userId, Long expirationMillis) { + Map<String, Object> customClaimMap = new HashMap<>(1); + customClaimMap.put(REFRESH_CLAIM, true); + return JsonWebTokenUtil.issueJwt(userId, expirationMillis, customClaimMap); + } + + private static String maskToken(String token) { + return token.substring(0, 4) + "****" + token.substring(token.length() - 4); + } + + private SubjectSum requireCurrentSubject() throws AuthenticationException { + SubjectSum subjectSum = SurenessContextHolder.getBindSubject(); + if (subjectSum == null) { + throw new AuthenticationException("No login user"); + } + return subjectSum; + } + + private String getCurrentUserId(SubjectSum subjectSum) { + Object principal = subjectSum.getPrincipal(); + return principal == null ? null : String.valueOf(principal); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AppServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AppServiceImpl.java new file mode 100644 index 0000000..32eb0ba --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AppServiceImpl.java @@ -0,0 +1,750 @@ +/* + * 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.manager.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.RuntimeParamDefine; +import org.apache.hertzbeat.common.entity.manager.Define; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.HertzBeatKeywordsUtil; +import org.apache.hertzbeat.common.util.JexlCheckerUtil; +import org.apache.hertzbeat.manager.dao.DefineDao; +import org.apache.hertzbeat.manager.dao.MonitorDao; +import org.apache.hertzbeat.manager.dao.ParamDao; +import org.apache.hertzbeat.manager.pojo.dto.Hierarchy; +import org.apache.hertzbeat.manager.pojo.dto.ObjectStoreConfigChangeEvent; +import org.apache.hertzbeat.manager.pojo.dto.ObjectStoreDTO; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.apache.hertzbeat.manager.pojo.dto.TemplateConfig; +import org.apache.hertzbeat.manager.service.AppService; +import org.apache.hertzbeat.manager.service.MonitorService; +import org.apache.hertzbeat.manager.service.ObjectStoreService; +import org.apache.hertzbeat.warehouse.service.WarehouseService; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.EventListener; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static java.util.Objects.isNull; + +/** + * Monitoring Type Management Implementation + * Temporarily stores the monitoring configuration and parameter configuration in memory, + * and then persists it to a storage system. + */ +@Service +@Order(value = Ordered.HIGHEST_PRECEDENCE) +@Slf4j +public class AppServiceImpl implements AppService, InitializingBean { + + private static final String PUSH_PROTOCOL_METRICS_NAME = "metrics"; + + private final MonitorDao monitorDao; + private final ObjectStoreConfigServiceImpl objectStoreConfigService; + private final ParamDao paramDao; + private final DefineDao defineDao; + private final WarehouseService warehouseService; + private final ObjectProvider<MonitorService> monitorServiceProvider; + private final ObjectProvider<ObjectStoreService> objectStoreServiceProvider; + + private final Map<String, Job> appDefines = new ConcurrentHashMap<>(); + private AppDefineStore appDefineStore; + private final AppDefineStore jarAppDefineStore = new JarAppDefineStoreImpl(); + + /** + * warehouseService is marked @Lazy to prevent potential circular dependencies. + */ + public AppServiceImpl(MonitorDao monitorDao, + ObjectStoreConfigServiceImpl objectStoreConfigService, + ParamDao paramDao, + DefineDao defineDao, + @Lazy WarehouseService warehouseService, + ObjectProvider<MonitorService> monitorServiceProvider, + ObjectProvider<ObjectStoreService> objectStoreServiceProvider) { + this.monitorDao = monitorDao; + this.objectStoreConfigService = objectStoreConfigService; + this.paramDao = paramDao; + this.defineDao = defineDao; + this.warehouseService = warehouseService; + this.monitorServiceProvider = monitorServiceProvider; + this.objectStoreServiceProvider = objectStoreServiceProvider; + } + + @Override + public List<ParamDefineInfo> getAppParamDefines(String app) { + if (StringUtils.isNotBlank(app)){ + var appDefine = appDefines.get(app.toLowerCase()); + if (appDefine != null && appDefine.getParams() != null) { + return appDefine.getParams().stream().map(ParamDefineInfo::fromRuntime).toList(); + } + } + return Collections.emptyList(); + } + + @Override + public Job getPushDefine(Long monitorId) throws IllegalArgumentException { + Job appDefine = appDefines.get(DispatchConstants.PROTOCOL_PUSH); + if (appDefine == null) { + throw new IllegalArgumentException("The push collector not support."); + } + List<Metrics> metrics = appDefine.getMetrics(); + List<Metrics> metricsTmp = new ArrayList<>(); + for (Metrics metric : metrics) { + if (PUSH_PROTOCOL_METRICS_NAME.equals(metric.getName())) { + List<Param> params = paramDao.findParamsByMonitorId(monitorId); + List<Configmap> configmaps = params.stream() + .map(param -> new Configmap(param.getField(), param.getParamValue(), + param.getType())).toList(); + Map<String, Configmap> configmap = configmaps.stream().collect(Collectors.toMap(Configmap::getKey, item -> item, (key1, key2) -> key1)); + CollectUtil.replaceFieldsForPushStyleMonitor(metric, configmap); + metricsTmp.add(metric); + } + } + appDefine.setMetrics(metricsTmp); + return appDefine; + } + + @Override + public Job getAutoGenerateDynamicDefine(Long monitorId) { + Job job = getAppDefine(DispatchConstants.PROTOCOL_PROMETHEUS); + List<CollectRep.MetricsData> metricsDataList = warehouseService.queryMonitorMetricsData(monitorId); + Metrics tmpMetrics = job.getMetrics().get(0); + List<Metrics> metricsList = new LinkedList<>(); + for (CollectRep.MetricsData metricsData : metricsDataList) { + List<Metrics.Field> fields = metricsData.getFields().stream().map(item -> + Metrics.Field.builder() + .field(item.getName()) + .type((byte) item.getType()) + .label(item.getLabel()) + .unit(item.getUnit()) + .build()) + .collect(Collectors.toList()); + Metrics metrics = Metrics.builder() + .visible(true) + .name(metricsData.getMetrics()) + .fields(fields) + .prometheus(tmpMetrics.getPrometheus()) + .build(); + metricsList.add(metrics); + } + job.setMetrics(metricsList); + return job; + } + + @Override + public Job getAppDefine(String app) throws IllegalArgumentException { + if (StringUtils.isBlank(app)) { + throw new IllegalArgumentException("The app can not null."); + } + var appDefine = appDefines.get(app.toLowerCase()); + if (appDefine == null) { + throw new IllegalArgumentException("The app " + app + " not support."); + } + return appDefine.clone(); + } + + @Override + public Optional<Job> getAppDefineOption(String app) { + if (StringUtils.isNotBlank(app)) { + Job appDefine = appDefines.get(app.toLowerCase()); + return Optional.ofNullable(appDefine); + } + return Optional.empty(); + } + + @Override + public List<String> getAppDefineMetricNames(String app) { + List<String> metricNames = new ArrayList<>(16); + if (StringUtils.isNotBlank(app)) { + var appDefine = appDefines.get(app.toLowerCase()); + if (appDefine == null) { + throw new IllegalArgumentException("The app " + app + " not support."); + } + metricNames.addAll(appDefine.getMetrics().stream().map(Metrics::getName).toList()); + } else { + appDefines.forEach((k, v) -> + metricNames.addAll(v.getMetrics().stream().map(Metrics::getName).toList())); + } + return metricNames; + } + + @Override + public Map<String, String> getI18nResources(String lang) { + Map<String, String> i18nMap = new HashMap<>(128); + for (var job : appDefines.values()) { + var name = job.getName(); + var i18nName = CommonUtil.getLangMappingValueFromI18nMap(lang, name); + if (i18nName != null) { + i18nMap.put("monitor.app." + job.getApp(), i18nName); + } + var help = job.getHelp(); + var i18nHelp = CommonUtil.getLangMappingValueFromI18nMap(lang, help); + if (i18nHelp != null) { + i18nMap.put("monitor.app." + job.getApp() + ".help", i18nHelp); + } + + var helpLink = job.getHelpLink(); + var i18nHelpLink = CommonUtil.getLangMappingValueFromI18nMap(lang, helpLink); + if (i18nHelpLink != null) { + i18nMap.put("monitor.app." + job.getApp() + ".helpLink", i18nHelpLink); + } + + for (var paramDefine : job.getParams()) { + var paramDefineName = paramDefine.getName(); + var i18nParamName = CommonUtil.getLangMappingValueFromI18nMap(lang, paramDefineName); + if (i18nParamName != null) { + i18nMap.put("monitor.app." + job.getApp() + ".param." + paramDefine.getField(), i18nParamName); + } + } + for (var metrics : job.getMetrics()) { + var metricsI18nName = metrics.getI18n(); + var i18nMetricsName = CommonUtil.getLangMappingValueFromI18nMap(lang, metricsI18nName); + if (i18nMetricsName != null) { + i18nMap.put("monitor.app." + job.getApp() + ".metrics." + metrics.getName(), i18nMetricsName); + } + if (metrics.getFields() == null) { + continue; + } + for (var field : metrics.getFields()) { + var fieldI18nName = field.getI18n(); + var i18nMetricName = CommonUtil.getLangMappingValueFromI18nMap(lang, fieldI18nName); + if (i18nMetricName != null) { + i18nMap.put("monitor.app." + job.getApp() + ".metrics." + metrics.getName() + ".metric." + field.getField(), i18nMetricName); + } + } + } + } + return i18nMap; + } + + @Override + public Map<String, String> getI18nApps(String lang) { + Map<String, String> i18nMap = new HashMap<>(128); + for (var job : appDefines.values()) { + var name = job.getName(); + var i18nName = CommonUtil.getLangMappingValueFromI18nMap(lang, name); + if (i18nName != null) { + i18nMap.put(job.getApp(), i18nName); + } + } + return i18nMap; + } + + @Override + public List<Hierarchy> getAllAppHierarchy(String lang) { + LinkedList<Hierarchy> hierarchies = new LinkedList<>(); + for (var job : appDefines.values()) { + queryAppHierarchy(lang, hierarchies, job); + } + return hierarchies; + } + + @Override + public List<Hierarchy> getAppHierarchy(String app, String lang) { + LinkedList<Hierarchy> hierarchies = new LinkedList<>(); + Job job = appDefines.get(app.toLowerCase()); + if (DispatchConstants.PROTOCOL_PUSH.equalsIgnoreCase(job.getApp())) { + return hierarchies; + } + queryAppHierarchy(lang, hierarchies, job); + return hierarchies; + } + + private void queryAppHierarchy(String lang, LinkedList<Hierarchy> hierarchies, Job job) { + var hierarchyApp = new Hierarchy(); + hierarchyApp.setCategory(job.getCategory()); + hierarchyApp.setValue(job.getApp()); + hierarchyApp.setHide(job.isHide()); + var nameMap = job.getName(); + if (nameMap != null && !nameMap.isEmpty()) { + var i18nName = CommonUtil.getLangMappingValueFromI18nMap(lang, nameMap); + if (i18nName != null) { + hierarchyApp.setLabel(i18nName); + } + } + List<Hierarchy> hierarchyMetricList = new LinkedList<>(); + if (DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(job.getApp())) { + List<Monitor> monitors = monitorDao.findMonitorsByAppEquals(job.getApp()); + for (Monitor monitor : monitors) { + List<CollectRep.MetricsData> metricsDataList = warehouseService.queryMonitorMetricsData(monitor.getId()); + for (CollectRep.MetricsData metricsData : metricsDataList) { + var hierarchyMetric = new Hierarchy(); + hierarchyMetric.setValue(metricsData.getMetrics()); + hierarchyMetric.setLabel(metricsData.getMetrics()); + List<Hierarchy> hierarchyFieldList = metricsData.getFields().stream() + .map(item -> { + var hierarchyField = new Hierarchy(); + hierarchyField.setValue(item.getName()); + hierarchyField.setLabel(item.getName()); + hierarchyField.setIsLeaf(true); + hierarchyField.setType((byte) item.getType()); + hierarchyField.setUnit(item.getUnit()); + return hierarchyField; + }).collect(Collectors.toList()); + hierarchyMetric.setChildren(hierarchyFieldList); + combineHierarchyMetrics(hierarchyMetricList, hierarchyMetric); + } + } + hierarchyApp.setChildren(hierarchyMetricList); + hierarchies.addFirst(hierarchyApp); + } else { + if (job.getMetrics() != null) { + for (var metrics : job.getMetrics()) { + var hierarchyMetric = new Hierarchy(); + hierarchyMetric.setValue(metrics.getName()); + var metricsI18nName = CommonUtil.getLangMappingValueFromI18nMap(lang, metrics.getI18n()); + hierarchyMetric.setLabel(metricsI18nName != null ? metricsI18nName : metrics.getName()); + List<Hierarchy> hierarchyFieldList = new LinkedList<>(); + if (metrics.getFields() != null) { + for (var field : metrics.getFields()) { + var hierarchyField = new Hierarchy(); + hierarchyField.setValue(field.getField()); + var metricI18nName = CommonUtil.getLangMappingValueFromI18nMap(lang, field.getI18n()); + hierarchyField.setLabel(metricI18nName != null ? metricI18nName : field.getField()); + hierarchyField.setIsLeaf(true); + hierarchyField.setType(field.getType()); + hierarchyField.setUnit(field.getUnit()); + hierarchyFieldList.add(hierarchyField); + } + hierarchyMetric.setChildren(hierarchyFieldList); + } + hierarchyMetricList.add(hierarchyMetric); + } + } + hierarchyApp.setChildren(hierarchyMetricList); + hierarchies.add(hierarchyApp); + } + } + + private void combineHierarchyMetrics(List<Hierarchy> hierarchyMetricList, Hierarchy hierarchyMetric) { + Optional<Hierarchy> preHierarchyOptional = hierarchyMetricList.stream() + .filter(item -> item.getValue().equals(hierarchyMetric.getValue())) + .findFirst(); + + if (preHierarchyOptional.isPresent()) { + Hierarchy preHierarchy = preHierarchyOptional.get(); + List<Hierarchy> children = preHierarchy.getChildren(); + Set<String> childrenKey = children.stream() + .map(Hierarchy::getValue) + .collect(Collectors.toSet()); + + for (Hierarchy child : hierarchyMetric.getChildren()) { + if (!childrenKey.contains(child.getValue())) { + children.add(child); + } + } + } else { + hierarchyMetricList.add(hierarchyMetric); + } + } + + @Override + public Map<String, Job> getAllAppDefines() { + return appDefines; + } + + @Override + public String getMonitorDefineFileContent(String app) { + var appDefine = appDefineStore.loadAppDefine(app); + if (isNull(appDefine)) { + appDefine = jarAppDefineStore.loadAppDefine(app); + } + if (isNull(appDefine)) { + throw new IllegalArgumentException("can not find " + app + " define yml"); + } + return appDefine; + } + + @Override + public void applyMonitorDefineYml(String ymlContent, boolean isModify) { + var yaml = new Yaml(); + Job app; + try { + app = yaml.loadAs(ymlContent, Job.class); + } catch (Exception e) { + log.error(e.getMessage()); + throw new IllegalArgumentException("parse yml error: " + e.getMessage()); + } + verifyDefineAppContent(app, isModify); + appDefineStore.save(app.getApp(), ymlContent); + Job originalJob = appDefines.get(app.getApp().toLowerCase()); + if (Objects.nonNull(originalJob)) { + boolean hide = originalJob.isHide(); + app.setHide(hide); + } + appDefines.put(app.getApp().toLowerCase(), app); + getMonitorService().updateAppCollectJob(app); + } + + private void verifyDefineAppContent(Job app, boolean isModify) { + Assert.notNull(app, "monitoring template can not null"); + Assert.notNull(app.getApp(), "monitoring template require attributes app"); + Assert.notNull(app.getCategory(), "monitoring template require attributes category"); + Assert.notEmpty(app.getName(), "monitoring template require attributes name"); + Assert.notEmpty(app.getParams(), "monitoring template require attributes params"); + var hasParamHost = app.getParams().stream().anyMatch(item -> "host".equals(item.getField())); + Assert.isTrue(hasParamHost, "monitoring template attributes params must have param host"); + Assert.notEmpty(app.getMetrics(), "monitoring template require attributes metrics"); + var hasAvailableMetrics = app.getMetrics().stream().anyMatch(item -> item.getPriority() == 0); + Assert.isTrue(hasAvailableMetrics, "monitoring template metrics list must have one priority 0 metrics"); + CommonUtil.validDefineI18n(app.getName(), "name"); + CommonUtil.validDefineI18n(app.getHelp(), "help"); + CommonUtil.validDefineI18n(app.getHelpLink(), "helpLink"); + for (RuntimeParamDefine param : app.getParams()) { + CommonUtil.validDefineI18n(param.getName(), param.getField() + " param"); + } + if (!isModify) { + Assert.isNull(appDefines.get(app.getApp().toLowerCase()), + "monitoring template name " + app.getApp() + " already exists."); + } + Set<String> fieldsSet = new HashSet<>(16); + for (Metrics metrics : app.getMetrics()) { + CommonUtil.validDefineI18n(metrics.getI18n(), metrics.getName() + " metric"); + Assert.notEmpty(metrics.getFields(), "monitoring template metrics fields can not null"); + fieldsSet.clear(); + for (Metrics.Field field : metrics.getFields()) { + CommonUtil.validDefineI18n(field.getI18n(), metrics.getName() + " metric " + field.getField() + " field"); + HertzBeatKeywordsUtil.verifyKeywords(field.getField()); + if (fieldsSet.contains(field.getField())) { + throw new IllegalArgumentException(app.getApp() + " " + metrics.getName() + " " + + field.getField() + " can not duplicated."); + } + if (JexlCheckerUtil.verifyKeywords(field.getField())) { + throw new IllegalArgumentException(app.getApp() + " " + metrics.getName() + " " + + field.getField() + " prohibited keywords."); + } + if (JexlCheckerUtil.verifyStartCharacter(field.getField())) { + throw new IllegalArgumentException(app.getApp() + " " + metrics.getName() + " " + + field.getField() + " illegal start character."); + } + if (JexlCheckerUtil.verifySpaces(field.getField())) { + throw new IllegalArgumentException(app.getApp() + " " + metrics.getName() + " " + + field.getField() + " no spaces allowed."); + } + fieldsSet.add(field.getField()); + } + } + } + + private MonitorService getMonitorService() { + if (monitorServiceProvider == null) { + throw new IllegalStateException("MonitorService provider is not available."); + } + MonitorService monitorService = monitorServiceProvider.getIfAvailable(); + if (monitorService == null) { + throw new IllegalStateException("MonitorService bean is not available."); + } + return monitorService; + } + + private ObjectStoreService getObjectStoreService() { + if (objectStoreServiceProvider == null) { + throw new IllegalStateException("ObjectStoreService provider is not available."); + } + ObjectStoreService objectStoreService = objectStoreServiceProvider.getIfAvailable(); + if (objectStoreService == null) { + throw new IllegalStateException("ObjectStoreService bean is not available."); + } + return objectStoreService; + } + + @Override + public void deleteMonitorDefine(String app) { + var monitors = monitorDao.findMonitorsByAppEquals(app); + if (monitors != null && !monitors.isEmpty()) { + throw new IllegalArgumentException("Can not delete define which has monitoring instances."); + } + appDefineStore.delete(app); + } + + @Override + public void updateCustomTemplateConfig(TemplateConfig config) { + if (config == null) { + return; + } + Map<String, TemplateConfig.AppTemplate> templateMap = config.getApps(); + if (templateMap == null || templateMap.isEmpty()) { + return; + } + for (var entry : templateMap.entrySet()) { + var app = entry.getKey().toLowerCase(); + var appTemplate = entry.getValue(); + if (appTemplate == null) { + continue; + } + var appDefine = appDefines.get(app); + if (appDefine == null) { + continue; + } + appDefine.setHide(appTemplate.isHide()); + } + } + + @Override + public void afterPropertiesSet() throws Exception { + // Guaranteed to be non-null due to constructor injection + var objectStoreConfig = objectStoreConfigService.getConfig(); + refreshStore(objectStoreConfig); + } + + @EventListener(ObjectStoreConfigChangeEvent.class) + public void onObjectStoreConfigChange(ObjectStoreConfigChangeEvent event) { + refreshStore(event.getConfig()); + } + + private void refreshStore(ObjectStoreDTO<?> objectStoreConfig) { + if (objectStoreConfig == null) { + appDefineStore = new DatabaseAppDefineStoreImpl(); + } else { + if (objectStoreConfig.getType() == ObjectStoreDTO.Type.OBS) { + appDefineStore = new ObjectStoreAppDefineStoreImpl(); + } else if (objectStoreConfig.getType() == ObjectStoreDTO.Type.DATABASE) { + appDefineStore = new DatabaseAppDefineStoreImpl(); + } else { + appDefineStore = new LocalFileAppDefineStoreImpl(); + } + } + jarAppDefineStore.loadAppDefines(); + appDefineStore.loadAppDefines(); + } + + private interface AppDefineStore { + + /** + * The configuration of all collection tasks is loaded + * + */ + boolean loadAppDefines(); + + /** + * Load a collection task configuration + * + * @param app app name + * @return collect task configuration text + */ + String loadAppDefine(String app); + + void save(String app, String ymlContent); + + void delete(String app); + } + + private class JarAppDefineStoreImpl implements AppDefineStore { + @Override + public boolean loadAppDefines() { + try { + Yaml yaml = new Yaml(); + log.info("load define app yml in internal jar"); + var resolver = new PathMatchingResourcePatternResolver(); + var resources = resolver.getResources("classpath:define/*.yml"); + for (var resource : resources) { + try (var inputStream = resource.getInputStream()) { + var app = yaml.loadAs(inputStream, Job.class); + appDefines.put(app.getApp().toLowerCase(), app); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + return true; + } catch (IOException e) { + log.error("define app yml not exist"); + return false; + } + } + + @Override + public String loadAppDefine(String app) { + var resolver = new PathMatchingResourcePatternResolver(); + var resource = resolver.getResource("classpath:define/app-" + app + ".yml"); + try (var inputStream = resource.getInputStream()) { + return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + } catch (IOException e) { + log.error(e.getMessage()); + return null; + } + } + + @Override + public void save(String app, String ymlContent) { + throw new UnsupportedOperationException(); + } + + @Override + public void delete(String app) { + throw new UnsupportedOperationException(); + } + } + + private class LocalFileAppDefineStoreImpl implements AppDefineStore { + @Override + public boolean loadAppDefines() { + var rootUrl = this.getClass().getClassLoader().getResource(""); + if (rootUrl == null) return false; + var directory = new File(rootUrl.getPath() + "define"); + if (!directory.exists()) return false; + Yaml yaml = new Yaml(); + for (var appFile : Objects.requireNonNull(directory.listFiles())) { + if (appFile.isFile() && (appFile.getName().endsWith("yml") || appFile.getName().endsWith("yaml"))) { + try (var is = new FileInputStream(appFile)) { + var app = yaml.loadAs(is, Job.class); + if (app != null) appDefines.put(app.getApp().toLowerCase(), app); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + } + return true; + } + + @Override + public String loadAppDefine(String app) { + var rootUrl = this.getClass().getClassLoader().getResource(""); + if (rootUrl == null) return null; + var file = new File(rootUrl.getPath() + "define" + File.separator + "app-" + app + ".yml"); + try { + return file.exists() ? FileUtils.readFileToString(file, StandardCharsets.UTF_8) : null; + } catch (Exception e) { + return null; + } + } + + @Override + public void save(String app, String ymlContent) { + var rootUrl = this.getClass().getClassLoader().getResource(""); + if (rootUrl == null) return; + var file = new File(rootUrl.getPath() + "define" + File.separator + "app-" + app + ".yml"); + try { + FileUtils.writeStringToFile(file, ymlContent, StandardCharsets.UTF_8, false); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void delete(String app) { + var rootUrl = this.getClass().getClassLoader().getResource(""); + if (rootUrl == null) return; + var file = new File(rootUrl.getPath() + "define" + File.separator + "app-" + app + ".yml"); + if (file.exists()) file.delete(); + appDefines.remove(app.toLowerCase()); + } + } + + private class ObjectStoreAppDefineStoreImpl implements AppDefineStore { + @Override + public boolean loadAppDefines() { + var objectStoreService = getObjectStoreService(); + Yaml yaml = new Yaml(); + objectStoreService.list("define").forEach(it -> { + if (it.getInputStream() != null) { + var app = yaml.loadAs(it.getInputStream(), Job.class); + if (app != null) appDefines.put(app.getApp().toLowerCase(), app); + } + }); + return true; + } + + @Override + public String loadAppDefine(String app) { + var objectStoreService = getObjectStoreService(); + var file = objectStoreService.download("define/app-" + app + ".yml"); + try { + return file != null ? IOUtils.toString(file.getInputStream(), StandardCharsets.UTF_8) : null; + } catch (IOException e) { + return null; + } + } + + @Override + public void save(String app, String ymlContent) { + getObjectStoreService().upload("define/app-" + app + ".yml", IOUtils.toInputStream(ymlContent, StandardCharsets.UTF_8)); + } + + @Override + public void delete(String app) { + getObjectStoreService().remove("define/app-" + app + ".yml"); + appDefines.remove(app.toLowerCase()); + } + } + + private class DatabaseAppDefineStoreImpl implements AppDefineStore { + @Override + public boolean loadAppDefines() { + Yaml yaml = new Yaml(); + defineDao.findAll().forEach(define -> { + var app = yaml.loadAs(define.getContent(), Job.class); + if (app != null) appDefines.put(define.getApp().toLowerCase(), app); + }); + return true; + } + + @Override + public String loadAppDefine(String app) { + return defineDao.findById(app).map(Define::getContent).orElse(null); + } + + @Override + public void save(String app, String ymlContent) { + Define define = new Define(); + define.setApp(app); + define.setContent(ymlContent); + defineDao.save(define); + } + + @Override + public void delete(String app) { + defineDao.deleteById(app); + appDefines.remove(app.toLowerCase()); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java new file mode 100644 index 0000000..f28cb30 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java @@ -0,0 +1,210 @@ +/* + * 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.manager.service.impl; + +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Bulletin; +import org.apache.hertzbeat.manager.pojo.dto.BulletinMetricsData; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.manager.dao.BulletinDao; +import org.apache.hertzbeat.manager.service.BulletinService; +import org.apache.hertzbeat.manager.service.MonitorService; +import org.apache.hertzbeat.warehouse.store.realtime.RealTimeDataReader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Bulletin Service Implementation + */ +@Service +@Slf4j +@Transactional(rollbackFor = Exception.class) +public class BulletinServiceImpl implements BulletinService { + + private static final String NO_DATA = "No Data"; + + private static final String EMPTY_STRING = ""; + + @Autowired + private BulletinDao bulletinDao; + + @Autowired + private MonitorService monitorService; + + @Autowired + private RealTimeDataReader realTimeDataReader; + + /** + * validate Bulletin + */ + @Override + public void validate(Bulletin bulletin) throws IllegalArgumentException { + if (bulletin == null) { + throw new IllegalArgumentException("Bulletin cannot be null"); + } + if (bulletin.getApp() == null || bulletin.getApp().isEmpty()) { + throw new IllegalArgumentException("Bulletin app cannot be null or empty"); + } + if (bulletin.getFields() == null || bulletin.getFields().isEmpty()) { + throw new IllegalArgumentException("Bulletin fields cannot be null or empty"); + } + if (bulletin.getMonitorIds() == null || bulletin.getMonitorIds().isEmpty()) { + throw new IllegalArgumentException("Bulletin monitorIds cannot be null or empty"); + } + Bulletin existBulletin = bulletinDao.findByName(bulletin.getName()); + if (existBulletin != null && !existBulletin.getId().equals(bulletin.getId())) { + throw new IllegalArgumentException("Bulletin name duplicated"); + } + } + + /** + * Save Bulletin + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void editBulletin(Bulletin bulletin) { + Optional<Bulletin> optional = bulletinDao.findById(bulletin.getId()); + if (optional.isEmpty()) { + throw new IllegalArgumentException("Bulletin not found"); + } + bulletinDao.save(bulletin); + } + + /** + * Add Bulletin + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void addBulletin(Bulletin bulletin) { + bulletinDao.save(bulletin); + } + + /** + * deal with the bulletin + */ + @Override + public BulletinMetricsData buildBulletinMetricsData(Long id) { + Optional<Bulletin> optional = bulletinDao.findById(id); + if (optional.isEmpty()) { + throw new IllegalArgumentException("Bulletin not found"); + } + Bulletin bulletin = optional.get(); + BulletinMetricsData.BulletinMetricsDataBuilder contentBuilder = BulletinMetricsData.builder() + .name(bulletin.getName()); + List<BulletinMetricsData.Data> dataList = new ArrayList<>(); + for (Long monitorId : bulletin.getMonitorIds()) { + Monitor monitor = monitorService.getMonitor(monitorId); + if (null == monitor) { + continue; + } + + BulletinMetricsData.Data.DataBuilder dataBuilder = BulletinMetricsData.Data.builder() + .monitorId(monitorId) + .monitorName(monitor.getName()) + .host(monitor.getInstance()); + + List<BulletinMetricsData.Metric> metrics = new ArrayList<>(); + Map<String, List<String>> fieldMap = bulletin.getFields(); + + if (fieldMap != null) { + for (Map.Entry<String, List<String>> entry : fieldMap.entrySet()) { + String metric = entry.getKey(); + List<String> fields = entry.getValue(); + BulletinMetricsData.Metric.MetricBuilder metricBuilder = BulletinMetricsData.Metric.builder() + .name(metric); + CollectRep.MetricsData currentMetricsData = realTimeDataReader.getCurrentMetricsData(monitorId, metric); + + List<List<BulletinMetricsData.Field>> fieldsList; + if (currentMetricsData != null) { + fieldsList = currentMetricsData.getValues().stream() + .map(valueRow -> { + List<BulletinMetricsData.Field> fieldList = currentMetricsData.getFields().stream() + .map(field -> BulletinMetricsData.Field.builder() + .key(field.getName()) + .unit(field.getUnit()) + .build()) + .toList(); + + for (int i = 0; i < fieldList.size(); i++) { + fieldList.get(i).setValue(valueRow.getColumns(i)); + } + return fieldList.stream().filter(field -> fields.contains(field.getKey())).toList(); + }) + .toList(); + } else { + fieldsList = Collections.singletonList(fields.stream() + .map(field -> BulletinMetricsData.Field.builder() + .key(field) + .unit(EMPTY_STRING) + .value(NO_DATA) + .build()) + .toList()); + } + + metricBuilder.fields(fieldsList); + metrics.add(metricBuilder.build()); + } + } + dataBuilder.metrics(metrics); + dataList.add(dataBuilder.build()); + } + contentBuilder.content(dataList); + return contentBuilder.build(); + } + + @Override + public Page<Bulletin> getBulletins(String search, Integer pageIndex, Integer pageSize) { + pageIndex = pageIndex == null ? 0 : pageIndex; + pageSize = pageSize == null ? Integer.MAX_VALUE : pageSize; + Specification<Bulletin> specification = (root, query, criteriaBuilder) -> { + Predicate predicate = criteriaBuilder.conjunction(); + if (StringUtils.isNotBlank(search)) { + Predicate predicateName = criteriaBuilder.like(root.get("name"), "%" + search + "%"); + predicate = criteriaBuilder.and(predicateName); + } + return predicate; + }; + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize); + return bulletinDao.findAll(specification, pageRequest); + } + + @Override + public void deleteBulletins(List<Long> ids) { + bulletinDao.deleteAllById(ids); + } + + /** + * Get Bulletin by id + */ + @Override + public Optional<Bulletin> getBulletinById(Long id) { + return bulletinDao.findById(id); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/CollectorServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/CollectorServiceImpl.java new file mode 100644 index 0000000..e441a47 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/CollectorServiceImpl.java @@ -0,0 +1,147 @@ +/* + * 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.manager.service.impl; + +import jakarta.persistence.criteria.Predicate; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.entity.manager.Collector; +import org.apache.hertzbeat.common.entity.manager.CollectorMonitorBind; +import org.apache.hertzbeat.common.support.exception.CommonException; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.apache.hertzbeat.manager.dao.CollectorDao; +import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao; +import org.apache.hertzbeat.manager.pojo.dto.CollectorInfo; +import org.apache.hertzbeat.manager.pojo.dto.CollectorSummary; +import org.apache.hertzbeat.manager.scheduler.AssignJobs; +import org.apache.hertzbeat.manager.scheduler.ConsistentHash; +import org.apache.hertzbeat.manager.scheduler.netty.ManageServer; +import org.apache.hertzbeat.manager.service.CollectorService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * collector service impl + */ +@Service +@Transactional(rollbackFor = Exception.class) +public class CollectorServiceImpl implements CollectorService { + + @Autowired + private CollectorDao collectorDao; + + @Autowired + private CollectorMonitorBindDao collectorMonitorBindDao; + + @Autowired + private ConsistentHash consistentHash; + + @Autowired(required = false) + private ManageServer manageServer; + + @Override + @Transactional(readOnly = true) + public Page<CollectorSummary> getCollectors(String name, int pageIndex, Integer pageSize) { + if (pageSize == null) { + pageSize = Integer.MAX_VALUE; + } + Specification<Collector> specification = (root, query, criteriaBuilder) -> { + Predicate predicate = criteriaBuilder.conjunction(); + if (StringUtils.isNotBlank(name)) { + Predicate predicateName = criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%" + name.toLowerCase() + "%"); + predicate = criteriaBuilder.and(predicateName); + } + return predicate; + }; + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize); + Page<Collector> collectors = collectorDao.findAll(specification, pageRequest); + List<CollectorSummary> collectorSummaryList = new LinkedList<>(); + for (Collector collector : collectors.getContent()) { + CollectorSummary.CollectorSummaryBuilder summaryBuilder = + CollectorSummary.builder().collector(CollectorInfo.fromEntity(collector)); + ConsistentHash.Node node = consistentHash.getNode(collector.getName()); + if (node != null && node.getAssignJobs() != null) { + AssignJobs assignJobs = node.getAssignJobs(); + summaryBuilder.pinMonitorNum(assignJobs.getPinnedJobs().size()); + summaryBuilder.dispatchMonitorNum(assignJobs.getJobs().size()); + } + collectorSummaryList.add(summaryBuilder.build()); + } + return new PageImpl<>(collectorSummaryList, pageRequest, collectors.getTotalElements()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteRegisteredCollector(List<String> collectors) { + if (CollectionUtils.isEmpty(collectors)) { + return; + } + // Determine whether there are fixed tasks on the collector + collectors.forEach(collector -> { + List<CollectorMonitorBind> binds = this.collectorMonitorBindDao.findCollectorMonitorBindsByCollector(collector); + if (CollectionUtils.isNotEmpty(binds)) { + throw new CommonException("The collector " + collector + " has pinned tasks that cannot be deleted."); + } + }); + collectors.forEach(collector -> { + this.manageServer.closeChannel(collector); + this.collectorDao.deleteCollectorByName(collector); + }); + } + + @Override + public boolean hasCollector(String collector) { + return this.collectorDao.findCollectorByName(collector).isPresent(); + } + + @Override + public Map<String, String> generateCollectorDeployInfo(String collector) { + if (hasCollector(collector)) { + throw new CommonException("There already exists a collector with same name."); + } + String host = IpDomainUtil.getLocalhostIp(); + Map<String, String> maps = new HashMap<>(6); + maps.put("identity", collector); + maps.put("host", host); + return maps; + } + + @Override + public void makeCollectorsOffline(List<String> collectors) { + if (CollectionUtils.isNotEmpty(collectors)) { + collectors.forEach(collector -> this.manageServer.getCollectorAndJobScheduler().offlineCollector(collector)); + } + } + + @Override + public void makeCollectorsOnline(List<String> collectors) { + if (CollectionUtils.isNotEmpty(collectors)) { + collectors.forEach(collector -> + this.manageServer.getCollectorAndJobScheduler().onlineCollector(collector)); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ConfigServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ConfigServiceImpl.java new file mode 100644 index 0000000..23b0297 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ConfigServiceImpl.java @@ -0,0 +1,82 @@ +/* + * 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.manager.service.impl; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.hertzbeat.manager.pojo.dto.TemplateConfig; +import org.apache.hertzbeat.manager.service.ConfigService; +import org.apache.hertzbeat.base.service.GeneralConfigService; +import org.springframework.stereotype.Component; + + +/** + * GeneralConfigService proxy class + */ +@Component +public class ConfigServiceImpl implements ConfigService { + + private static final String TEMPLATE_CONFIG_TYPE = "template"; + + private final Map<String, GeneralConfigService> configServiceMap; + + public ConfigServiceImpl(List<GeneralConfigService> generalConfigServices){ + configServiceMap = new ConcurrentHashMap<>(8); + if (CollectionUtils.isNotEmpty(generalConfigServices)) { + generalConfigServices.forEach(config -> configServiceMap.put(config.type(), config)); + } + } + + @Override + public void saveConfig(String type, Object config) { + GeneralConfigService configService = configServiceMap.get(type); + if (configService == null) { + throw new IllegalArgumentException("Not supported this config type: " + type); + } + configService.saveConfig(config); + } + + @Override + public Object getConfig(String type) { + GeneralConfigService configService = configServiceMap.get(type); + if (configService == null) { + throw new IllegalArgumentException("Not supported this config type: " + type); + } + return configService.getConfig(); + } + + @Override + public void updateTemplateAppConfig(String app, TemplateConfig.AppTemplate template){ + GeneralConfigService configService = configServiceMap.get(TEMPLATE_CONFIG_TYPE); + if (!(configService instanceof TemplateConfigServiceImpl)) { + throw new IllegalArgumentException("Not supported this config type: template"); + } + TemplateConfig config = ((TemplateConfigServiceImpl) configService).getConfig(); + if (config == null) { + config = new TemplateConfig(); + } + if (config.getApps() == null) { + config.setApps(new HashMap<>(8)); + } + config.getApps().put(app, template); + configService.saveConfig(config); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/DefaultPluginRunner.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/DefaultPluginRunner.java new file mode 100644 index 0000000..7add67d --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/DefaultPluginRunner.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.service.impl; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.plugin.PluginContext; +import org.apache.hertzbeat.manager.service.PluginService; +import org.apache.hertzbeat.plugin.runner.PluginRunner; +import org.springframework.stereotype.Service; + + +/** + * default plugin runner + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class DefaultPluginRunner implements PluginRunner { + + private final PluginService pluginService; + + @Override + public <T> void pluginExecute(Class<T> clazz, Consumer<T> execute) { + try { + pluginService.pluginExecute(clazz, execute); + } catch (Exception e) { + log.error("plugin execute failed", e); + } + } + + @Override + public <T> void pluginExecute(Class<T> clazz, BiConsumer<T, PluginContext> execute) { + try { + pluginService.pluginExecute(clazz, execute); + } catch (Exception e) { + log.error("plugin execute failed", e); + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ExcelImExportServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ExcelImExportServiceImpl.java new file mode 100644 index 0000000..59811ad --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ExcelImExportServiceImpl.java @@ -0,0 +1,318 @@ +/* + * 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.manager.service.impl; + +import static org.apache.hertzbeat.common.constants.ExportFileConstants.ExcelFile.FILE_SUFFIX; +import static org.apache.hertzbeat.common.constants.ExportFileConstants.ExcelFile.TYPE; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.RegionUtil; +import org.springframework.stereotype.Service; +import tools.jackson.core.type.TypeReference; + +/** + * Configure the import and export EXCEL format + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class ExcelImExportServiceImpl extends AbstractImExportServiceImpl{ + + /** + * Export file type + * @return file type + */ + @Override + public String type() { + return TYPE; + } + + /** + * Get Export File Name + * @return file name + */ + @Override + public String getFileName() { + return fileNamePrefix() + FILE_SUFFIX; + } + + /** + * Parsing an input stream into a form + * @param is input stream + * @return form + */ + @Override + public List<ExportMonitorDTO> parseImport(InputStream is) { + try (Workbook workbook = WorkbookFactory.create(is)) { + Sheet sheet = workbook.getSheetAt(0); + + List<ExportMonitorDTO> monitors = new ArrayList<>(); + List<Integer> startRowList = new ArrayList<>(); + + for (Row row : sheet) { + if (row.getRowNum() == 0) { + continue; + } + String name = getCellValueAsString(row.getCell(0)); + if (StringUtils.isNotBlank(name)) { + startRowList.add(row.getRowNum()); + MonitorDTO monitor = extractMonitorDataFromRow(row); + ExportMonitorDTO exportMonitor = new ExportMonitorDTO(); + exportMonitor.setMonitor(monitor); + monitors.add(exportMonitor); + } + } + List<List<ParamDTO>> paramsList = new ArrayList<>(); + for (int i = 0; i < startRowList.size(); i++) { + int startRowIndex = startRowList.get(i); + int endRowIndex = (i + 1 < startRowList.size()) ? startRowList.get(i + 1) : sheet.getLastRowNum() + 1; + List<ParamDTO> params = new ArrayList<>(); + + for (int j = startRowIndex; j < endRowIndex; j++) { + Row row = sheet.getRow(j); + if (row == null) { + continue; + } + ParamDTO param = extractParamDataFromRow(row); + if (param != null) { + params.add(param); + } + } + paramsList.add(params); + } + for (int i = 0; i < monitors.size(); i++) { + monitors.get(i).setParams(paramsList.get(i)); + } + return monitors; + } catch (IOException e) { + throw new RuntimeException("Failed to parse monitor data", e); + } + } + + private MonitorDTO extractMonitorDataFromRow(Row row) { + MonitorDTO monitor = new MonitorDTO(); + + monitor.setName(getCellValueAsString(row.getCell(0))); + monitor.setApp(getCellValueAsString(row.getCell(1))); + monitor.setHost(getCellValueAsString(row.getCell(2))); + monitor.setIntervals(getCellValueAsInteger(row.getCell(3))); + monitor.setStatus(getCellValueAsByte(row.getCell(4))); + monitor.setDescription(getCellValueAsString(row.getCell(5))); + + monitor.setLabels(parseJsonMap(getCellValueAsString(row.getCell(6)))); + monitor.setAnnotations(parseJsonMap(getCellValueAsString(row.getCell(7)))); + monitor.setScheduleType(getCellValueAsString(row.getCell(8))); + monitor.setCronExpression(getCellValueAsString(row.getCell(9))); + monitor.setCollector(getCellValueAsString(row.getCell(10))); + return monitor; + } + + private Map<String, String> parseJsonMap(String value) { + if (StringUtils.isNotBlank(value)) { + try { + TypeReference<Map<String, String>> typeReference = new TypeReference<>() {}; + return JsonUtil.fromJson(value, typeReference); + } catch (Exception ignored) {} + } + return null; + } + + private ParamDTO extractParamDataFromRow(Row row) { + String fieldName = getCellValueAsString(row.getCell(11)); + if (StringUtils.isNotBlank(fieldName)) { + ParamDTO param = new ParamDTO(); + param.setField(fieldName); + param.setType(getCellValueAsByte(row.getCell(12))); + param.setValue(getCellValueAsString(row.getCell(13))); + return param; + } + return null; + } + + private String getCellValueAsString(Cell cell) { + if (cell == null) { + return null; + } + return switch (cell.getCellType()) { + case STRING -> cell.getStringCellValue(); + case NUMERIC -> String.valueOf(cell.getNumericCellValue()); + default -> null; + }; + } + + private boolean getCellValueAsBoolean(Cell cell) { + if (cell == null) { + return false; + } + if (Objects.requireNonNull(cell.getCellType()) == CellType.BOOLEAN) { + return cell.getBooleanCellValue(); + } + return false; + } + + private Integer getCellValueAsInteger(Cell cell) { + if (cell == null) { + return null; + } + if (Objects.requireNonNull(cell.getCellType()) == CellType.NUMERIC) { + return (int) cell.getNumericCellValue(); + } + return null; + } + + private Byte getCellValueAsByte(Cell cell) { + if (cell == null) { + return null; + } + if (Objects.requireNonNull(cell.getCellType()) == CellType.NUMERIC) { + return (byte) cell.getNumericCellValue(); + } + return null; + } + + /** + * Export Configuration to Output Stream + * @param monitorList config list + * @param os output stream + */ + @Override + public void writeOs(List<ExportMonitorDTO> monitorList, OutputStream os) { + try { + + Workbook workbook = WorkbookFactory.create(true); + String sheetName = "Export Monitor"; + Sheet sheet = workbook.createSheet(sheetName); + sheet.setDefaultColumnWidth(20); + sheet.setColumnWidth(6, 40 * 256); + sheet.setColumnWidth(7, 40 * 256); + sheet.setColumnWidth(13, 40 * 256); + // set header style + CellStyle headerCellStyle = workbook.createCellStyle(); + Font headerFont = workbook.createFont(); + headerFont.setBold(true); + headerCellStyle.setFont(headerFont); + headerCellStyle.setAlignment(HorizontalAlignment.CENTER); + // set cell style + CellStyle cellStyle = workbook.createCellStyle(); + cellStyle.setAlignment(HorizontalAlignment.CENTER); + // set header + String[] headers = { "Name", "App", "Host", "Intervals", "Status", "Description", "Labels", "Annotations", + "ScheduleType", "CronExpression", "Collector", "Param-Field", "Param-Type", "Param-Value" }; + Row headerRow = sheet.createRow(0); + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + cell.setCellStyle(headerCellStyle); + } + + // foreach monitor, each monitor object corresponds to a row of data + int rowIndex = 1; + for (ExportMonitorDTO monitor : monitorList) { + // get monitor information + MonitorDTO monitorDTO = monitor.getMonitor(); + // get monitor parameters + List<ParamDTO> paramList = monitor.getParams(); + // merge monitor information and parameter information into one row + for (int i = 0; i < Math.max(paramList.size(), 1); i++) { + Row row = sheet.createRow(rowIndex++); + if (i == 0) { + // You need to fill in the monitoring information only once + Cell nameCell = row.createCell(0); + nameCell.setCellValue(monitorDTO.getName()); + nameCell.setCellStyle(cellStyle); + Cell appCell = row.createCell(1); + appCell.setCellValue(monitorDTO.getApp()); + appCell.setCellStyle(cellStyle); + Cell hostCell = row.createCell(2); + hostCell.setCellValue(monitorDTO.getHost()); + hostCell.setCellStyle(cellStyle); + Cell intervalsCell = row.createCell(3); + intervalsCell.setCellValue(monitorDTO.getIntervals()); + intervalsCell.setCellStyle(cellStyle); + Cell statusCell = row.createCell(4); + statusCell.setCellValue(monitorDTO.getStatus()); + statusCell.setCellStyle(cellStyle); + Cell descriptionCell = row.createCell(5); + descriptionCell.setCellValue(monitorDTO.getDescription()); + descriptionCell.setCellStyle(cellStyle); + Cell labelsCell = row.createCell(6); + labelsCell.setCellValue(JsonUtil.toJson(monitorDTO.getLabels())); + labelsCell.setCellStyle(cellStyle); + Cell annotationsCell = row.createCell(7); + annotationsCell.setCellValue(JsonUtil.toJson(monitorDTO.getAnnotations())); + annotationsCell.setCellStyle(cellStyle); + Cell scheduleTypeCell = row.createCell(8); + scheduleTypeCell.setCellValue(monitorDTO.getScheduleType()); + scheduleTypeCell.setCellStyle(cellStyle); + Cell cronExpressionCell = row.createCell(9); + cronExpressionCell.setCellValue(monitorDTO.getCronExpression()); + cronExpressionCell.setCellStyle(cellStyle); + Cell collectorCell = row.createCell(10); + collectorCell.setCellValue(monitorDTO.getCollector()); + collectorCell.setCellStyle(cellStyle); + } + // Fill in parameter information + if (i < paramList.size()) { + ParamDTO paramDTO = paramList.get(i); + Cell fieldCell = row.createCell(11); + fieldCell.setCellValue(paramDTO.getField()); + fieldCell.setCellStyle(cellStyle); + Cell typeCell = row.createCell(12); + typeCell.setCellValue(paramDTO.getType()); + typeCell.setCellStyle(cellStyle); + Cell valueCell = row.createCell(13); + valueCell.setCellValue(paramDTO.getValue()); + valueCell.setCellStyle(cellStyle); + } + } + if (CollectionUtils.isNotEmpty(paramList)) { + RegionUtil.setBorderTop(BorderStyle.THICK, new CellRangeAddress(rowIndex - paramList.size(), rowIndex - 1, 0, 13), sheet); + RegionUtil.setBorderBottom(BorderStyle.THICK, new CellRangeAddress(rowIndex - paramList.size(), rowIndex - 1, 0, 13), sheet); + RegionUtil.setBorderLeft(BorderStyle.THICK, new CellRangeAddress(rowIndex - paramList.size(), rowIndex - 1, 0, 13), sheet); + RegionUtil.setBorderRight(BorderStyle.THICK, new CellRangeAddress(rowIndex - paramList.size(), rowIndex - 1, 0, 13), sheet); + } + } + workbook.write(os); + os.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/JsonImExportServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/JsonImExportServiceImpl.java new file mode 100644 index 0000000..cbee812 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/JsonImExportServiceImpl.java @@ -0,0 +1,63 @@ +/* + * 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.manager.service.impl; + +import static org.apache.hertzbeat.common.constants.ExportFileConstants.JsonFile.FILE_SUFFIX; +import static org.apache.hertzbeat.common.constants.ExportFileConstants.JsonFile.TYPE; + +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.stereotype.Service; +import tools.jackson.core.type.TypeReference; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import lombok.extern.slf4j.Slf4j; + +/** + * Configure the import and export JSON format + */ +@Slf4j +@Service +public class JsonImExportServiceImpl extends AbstractImExportServiceImpl { + + @Override + public List<ExportMonitorDTO> parseImport(InputStream is) { + List<ExportMonitorDTO> result = JsonUtil.fromJson(is, new TypeReference<List<ExportMonitorDTO>>(){}); + if (result == null) { + throw new RuntimeException("Parse JSON failed"); + } + return result; + } + + @Override + public void writeOs(List<ExportMonitorDTO> monitorList, OutputStream os) { + JsonUtil.toJson(monitorList, os); + } + + @Override + public String type() { + return TYPE; + } + + @Override + public String getFileName() { + return fileNamePrefix() + FILE_SUFFIX; + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/LabelServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/LabelServiceImpl.java new file mode 100644 index 0000000..a8c6363 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/LabelServiceImpl.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.service.impl; + +import jakarta.persistence.criteria.Predicate; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.entity.manager.Label; +import org.apache.hertzbeat.base.dao.LabelDao; +import org.apache.hertzbeat.base.service.LabelService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Label service implementation. + */ +@Service +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class LabelServiceImpl implements LabelService { + + @Autowired + private LabelDao labelDao; + + @Override + public void addLabel(Label label) { + // Verify request data + Optional<Label> optional = labelDao.findLabelByNameAndTagValue(label.getName(), label.getTagValue()); + if (optional.isPresent()) { + throw new IllegalArgumentException("The label already exists."); + } + label.setType((byte) 1); + label.setId(null); + labelDao.save(label); + } + + @Override + public void modifyLabel(Label label) { + Optional<Label> optional = labelDao.findById(label.getId()); + if (optional.isPresent()) { + + Optional<Label> existOptional = labelDao.findLabelByNameAndTagValue(label.getName(), label.getTagValue()); + if (existOptional.isPresent() && !existOptional.get().getId().equals(label.getId())) { + throw new IllegalArgumentException("The label with same key and value already exists."); + } + label.setTagValue(StringUtils.isEmpty(label.getTagValue()) ? null : label.getTagValue()); + labelDao.save(label); + } else { + throw new IllegalArgumentException("The label is not existed"); + } + } + + @Override + public Page<Label> getLabels(String search, Byte type, int pageIndex, int pageSize) { + Specification<Label> specification = (root, query, criteriaBuilder) -> { + List<Predicate> andList = new ArrayList<>(); + if (type != null) { + Predicate predicateApp = criteriaBuilder.equal(root.get("type"), type); + andList.add(predicateApp); + } + Predicate[] andPredicates = new Predicate[andList.size()]; + Predicate andPredicate = criteriaBuilder.and(andList.toArray(andPredicates)); + + List<Predicate> orList = new ArrayList<>(); + if (StringUtils.isNotBlank(search)) { + Predicate predicateName = criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%" + search.toLowerCase() + "%"); + orList.add(predicateName); + Predicate predicateValue = criteriaBuilder.like(criteriaBuilder.lower(root.get("tagValue")), "%" + search.toLowerCase() + "%"); + orList.add(predicateValue); + } + Predicate[] orPredicates = new Predicate[orList.size()]; + Predicate orPredicate = criteriaBuilder.or(orList.toArray(orPredicates)); + + if (andPredicates.length == 0 && orPredicates.length == 0) { + return query.where().getRestriction(); + } else if (andPredicates.length == 0) { + return orPredicate; + } else if (orPredicates.length == 0) { + return andPredicate; + } else { + return query.where(andPredicate, orPredicate).getRestriction(); + } + }; + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize); + return labelDao.findAll(specification, pageRequest); + } + + @Override + public void deleteLabels(HashSet<Long> ids) { + if (CollectionUtils.isEmpty(ids)) { + return; + } + labelDao.deleteLabelsByIdIn(ids); + } + + @Override + public List<Label> determineNewLabels(Set<Map.Entry<String, String>> originLabels) { + + if (originLabels == null || originLabels.isEmpty()) return List.of(); + + // Get all labels from the database + Set<Label> allLabels = labelDao.findAll().stream() + .map(label -> Label.builder() + .name(label.getName()) + .tagValue(label.getTagValue()) + .build()) + .collect(Collectors.toSet()); + + // If the bound label (key:value) does not exist, then add it + return originLabels.stream() + .map(entry -> Label.builder() + .name(entry.getKey()) + .tagValue(entry.getValue()) + .build()) + .filter(label -> !allLabels.contains(label)) + .map(label -> Label.builder() + .id(null) + .name(label.getName()) + .tagValue(label.getTagValue()) + .type((byte) 0) + .build()) + .toList(); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MailGeneralConfigServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MailGeneralConfigServiceImpl.java new file mode 100644 index 0000000..2960730 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MailGeneralConfigServiceImpl.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.service.impl; + +import tools.jackson.core.type.TypeReference; +import java.lang.reflect.Type; +import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.common.entity.dto.MailServerConfig; +import org.springframework.stereotype.Service; + +/** + * MailGeneralConfigServiceImpl class is the implementation of general email configuration service, + * which inherits the AbstractGeneralConfigServiceImpl class. + */ + +@Service +public class MailGeneralConfigServiceImpl extends AbstractGeneralConfigServiceImpl<MailServerConfig> { + + public MailGeneralConfigServiceImpl(GeneralConfigDao generalConfigDao) { + super(generalConfigDao); + } + + @Override + public String type() { + return GeneralConfigTypeEnum.email.name(); + } + + /** + * This method is used to get the TypeReference of NoticeSender type for subsequent processing. + * a TypeReference of NoticeSender type + */ + @Override + public TypeReference<MailServerConfig> getTypeReference() { + return new TypeReference<>() { + @Override + public Type getType() { + return MailServerConfig.class; + } + }; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MetricsFavoriteServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MetricsFavoriteServiceImpl.java new file mode 100644 index 0000000..1129dfb --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MetricsFavoriteServiceImpl.java @@ -0,0 +1,91 @@ +/* + * 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.manager.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.entity.manager.MetricsFavorite; +import org.apache.hertzbeat.manager.dao.MetricsFavoriteDao; +import org.apache.hertzbeat.manager.service.MetricsFavoriteService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Metrics Favorite Service Implementation + */ +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(rollbackFor = Exception.class) +public class MetricsFavoriteServiceImpl implements MetricsFavoriteService { + + private final MetricsFavoriteDao metricsFavoriteDao; + + @Override + public void addMetricsFavorite(String creator, Long monitorId, String metricsName) { + Optional<MetricsFavorite> existing = metricsFavoriteDao + .findByCreatorAndMonitorIdAndMetricsName(creator, monitorId, metricsName); + if (existing.isPresent()) { + throw new RuntimeException("Metrics favorite already exists: " + metricsName); + } + MetricsFavorite favorite = MetricsFavorite.builder() + .creator(creator) + .monitorId(monitorId) + .metricsName(metricsName) + .createTime(LocalDateTime.now()) + .build(); + metricsFavoriteDao.save(favorite); + } + + @Override + public void removeMetricsFavorite(String userId, Long monitorId, String metricsName) { + metricsFavoriteDao.deleteByUserIdAndMonitorIdAndMetricsName(userId, monitorId, metricsName); + } + + @Override + @Transactional(readOnly = true) + public Set<String> getUserFavoritedMetrics(String userId, Long monitorId) { + if (null == userId || null == monitorId) { + return Set.of(); + } + List<MetricsFavorite> favorites = metricsFavoriteDao.findByCreatorAndMonitorId(userId, monitorId); + if (null == favorites || favorites.isEmpty()) { + return Set.of(); + } + return favorites.stream() + .map(MetricsFavorite::getMetricsName) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet()); + } + + @Override + public void deleteFavoritesByMonitorIdIn(Set<Long> monitorIds) { + if (null == monitorIds || monitorIds.isEmpty()) { + return; + } + metricsFavoriteDao.deleteFavoritesByMonitorIdIn(monitorIds); + } + +} \ No newline at end of file diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ModelProviderConfigServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ModelProviderConfigServiceImpl.java new file mode 100644 index 0000000..c445ec3 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ModelProviderConfigServiceImpl.java @@ -0,0 +1,61 @@ +/* + * 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.manager.service.impl; + +import tools.jackson.core.type.TypeReference; +import java.lang.reflect.Type; +import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum; +import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +/** + * llm model provider config service impl + */ +@Service +public class ModelProviderConfigServiceImpl extends AbstractGeneralConfigServiceImpl<ModelProviderConfig> { + + private final ApplicationContext applicationContext; + + public ModelProviderConfigServiceImpl(ApplicationContext applicationContext, GeneralConfigDao generalConfigDao) { + super(generalConfigDao); + this.applicationContext = applicationContext; + } + + @Override + public String type() { + return GeneralConfigTypeEnum.provider.name(); + } + + @Override + public void handler(ModelProviderConfig config) { + applicationContext.publishEvent(new AiProviderConfigChangeEvent(applicationContext)); + } + + @Override + public TypeReference<ModelProviderConfig> getTypeReference() { + return new TypeReference<>() { + @Override + public Type getType() { + return ModelProviderConfig.class; + } + }; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java new file mode 100644 index 0000000..63ed4fb --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java @@ -0,0 +1,949 @@ +/* + * 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.manager.service.impl; + +import com.google.common.collect.Sets; +import com.google.common.primitives.Longs; +import com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.SurenessContextHolder; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.AlertDefineBindDao; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.RuntimeParamDefine; +import org.apache.hertzbeat.common.entity.manager.Collector; +import org.apache.hertzbeat.common.entity.manager.CollectorMonitorBind; +import org.apache.hertzbeat.common.entity.manager.Label; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.MonitorBind; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.support.event.MonitorDeletedEvent; + +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.apache.hertzbeat.common.util.JexlCheckerUtil; +import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator; +import org.apache.hertzbeat.grafana.service.DashboardService; +import org.apache.hertzbeat.manager.component.validator.ParamValidatorManager; +import org.apache.hertzbeat.manager.dao.CollectorDao; +import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao; +import org.apache.hertzbeat.base.dao.LabelDao; +import org.apache.hertzbeat.manager.dao.MonitorBindDao; +import org.apache.hertzbeat.manager.dao.MonitorDao; +import org.apache.hertzbeat.manager.dao.ParamDao; +import org.apache.hertzbeat.manager.pojo.dto.AppCount; +import org.apache.hertzbeat.manager.pojo.dto.MetricsInfo; +import org.apache.hertzbeat.manager.pojo.dto.MonitorDto; +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.apache.hertzbeat.manager.scheduler.CollectJobScheduling; +import org.apache.hertzbeat.manager.service.AppService; +import org.apache.hertzbeat.base.service.LabelService; +import org.apache.hertzbeat.manager.service.MetricsFavoriteService; +import org.apache.hertzbeat.manager.service.MonitorService; +import org.apache.hertzbeat.manager.service.helper.MonitorImExportHelper; +import org.apache.hertzbeat.manager.support.exception.MonitorDatabaseException; +import org.apache.hertzbeat.manager.support.exception.MonitorDetectException; +import org.apache.hertzbeat.warehouse.service.WarehouseService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Monitoring and management service implementation + */ +@Service +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class MonitorServiceImpl implements MonitorService { + public static final String PATTERN_HTTP = "(?i)http://"; + public static final String PATTERN_HTTPS = "(?i)https://"; + private static final Long MONITOR_ID_TMP = 1000000000L; + private static final byte ALL_MONITOR_STATUS = 9; + public static final String PARAM_FIELD_PORT = "port"; + + @Autowired + private ParamValidatorManager paramValidatorManager; + @Autowired + private MonitorImExportHelper monitorImExportHelper; + @Autowired + private AppService appService; + @Autowired + private CollectJobScheduling collectJobScheduling; + @Autowired + private MonitorDao monitorDao; + @Autowired + private ParamDao paramDao; + @Autowired + private MonitorBindDao monitorBindDao; + @Autowired + private CollectorDao collectorDao; + @Autowired + private CollectorMonitorBindDao collectorMonitorBindDao; + @Autowired + private AlertDefineBindDao alertDefineBindDao; + @Autowired + private ApplicationContext applicationContext; + @Autowired + private WarehouseService warehouseService; + @Autowired + private DashboardService dashboardService; + @Autowired + private LabelDao labelDao; + @Autowired + private LabelService labelService; + @Autowired + private MetricsFavoriteService metricsFavoriteService; + + @Override + @Transactional(readOnly = true) + public void detectMonitor(Monitor monitor, List<Param> params, String collector) throws MonitorDetectException { + if (CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape())) { + detectMonitorDirectly(monitor, params, collector); + } else { + detectSdMonitor(monitor, params, collector); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void addMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard grafanaDashboard) + throws RuntimeException { + // Apply for monitor id + long monitorId = SnowFlakeIdGenerator.generateId(); + Map<String, String> labels = monitor.getLabels(); + if (labels == null) { + labels = new HashMap<>(8); + monitor.setLabels(labels); + } + List<Label> addLabels = labelService.determineNewLabels(labels.entrySet()); + + if (!addLabels.isEmpty()) { + labelDao.saveAll(addLabels); + } + + // Construct the collection task Job entity + boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) + || !StringUtils.hasText(monitor.getScrape()); + String app = isStatic ? monitor.getApp() : monitor.getScrape(); + Job appDefine = appService.getAppDefine(app); + if (!isStatic) { + appDefine.setSd(true); + monitor.setInstance("unknow"); + } + if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) { + appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName()); + } + appDefine.setMonitorId(monitorId); + appDefine.setDefaultInterval(monitor.getIntervals()); + appDefine.setCyclic(true); + appDefine.setTimestamp(System.currentTimeMillis()); + appDefine.setScheduleType(monitor.getScheduleType()); + appDefine.setCronExpression(monitor.getCronExpression()); + + String instance = monitor.getInstance(); + // The port field may be null + Param portParam = params.stream() + .filter(param -> PARAM_FIELD_PORT.equals(param.getField())) + .findFirst() + .orElse(null); + String portWithMark = (Objects.isNull(portParam) || !StringUtils.hasText(portParam.getParamValue())) + ? "" + : SignConstants.DOUBLE_MARK + portParam.getParamValue(); + if (!IpDomainUtil.isHasPortWithMark(instance)) { + instance = instance + portWithMark; + } + monitor.setInstance(instance); + + Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(), + CommonConstants.LABEL_INSTANCE, instance); + appDefine.setMetadata(metadata); + appDefine.setLabels(monitor.getLabels()); + appDefine.setAnnotations(monitor.getAnnotations()); + List<Configmap> configmaps = params.stream().map(param -> { + param.setMonitorId(monitorId); + return new Configmap(param.getField(), param.getParamValue(), param.getType()); + }).collect(Collectors.toList()); + appDefine.setConfigmap(configmaps); + long jobId = collector == null ? collectJobScheduling.addAsyncCollectJob(appDefine, null) + : collectJobScheduling.addAsyncCollectJob(appDefine, collector); + try { + detectMonitor(monitor, params, collector); + } catch (Exception ignored) { + } + + try { + if (collector != null) { + CollectorMonitorBind collectorMonitorBind = CollectorMonitorBind.builder() + .collector(collector) + .monitorId(monitorId) + .build(); + collectorMonitorBindDao.save(collectorMonitorBind); + } + monitor.setId(monitorId); + monitor.setJobId(jobId); + // create grafana dashboard + if (monitor.getApp().equals(CommonConstants.PROMETHEUS) && grafanaDashboard != null + && grafanaDashboard.isEnabled()) { + dashboardService.createOrUpdateDashboard(grafanaDashboard.getTemplate(), monitorId); + } + monitorDao.save(monitor); + paramDao.saveAll(params); + } catch (Exception e) { + log.error("Error while adding monitor: {}", e.getMessage(), e); + collectJobScheduling.cancelAsyncCollectJob(jobId); + throw new MonitorDatabaseException(e.getMessage()); + } + } + + @Override + public void export(List<Long> ids, String type, HttpServletResponse res) throws Exception { + monitorImExportHelper.export(ids, type, res); + } + + @Override + public void exportAll(String type, HttpServletResponse res) throws Exception { + // Get all monitor IDs from the database + List<Long> allMonitorIds = monitorDao.findAll() + .stream() + .map(Monitor::getId) + .collect(Collectors.toList()); + + // Use the existing export method to export all monitors + export(allMonitorIds, type, res); + } + + @Override + public void importConfig(MultipartFile file) throws Exception { + monitorImExportHelper.importConfig(file); + } + + @Override + @Transactional(readOnly = true) + public void validate(MonitorDto monitorDto, Boolean isModify) throws IllegalArgumentException { + // The request monitoring parameter matches the monitoring parameter definition + // mapping check + var monitorInfo = monitorDto.getMonitorInfo(); + monitorInfo.setInstance(StringUtils.hasText(monitorInfo.getInstance()) ? monitorInfo.getInstance().trim() : null); + monitorInfo.setName(monitorInfo.getName().trim()); + Monitor monitor = monitorInfo.toEntity(); + Map<String, MonitorParam> paramMap = monitorDto.getParamInfos() + .stream() + .peek(param -> { + param.setMonitorId(monitor.getId()); + String value = param.getParamValue() == null ? null : param.getParamValue().trim(); + param.setParamValue(value); + }) + .collect(Collectors.toMap(MonitorParam::getField, param -> param)); + // Check name uniqueness and can not equal app type + if (isModify != null) { + Optional<Job> defineOptional = appService.getAppDefineOption(monitor.getName()); + if (defineOptional.isPresent()) { + throw new IllegalArgumentException("Monitoring name cannot be the existed monitoring type name!"); + } + Optional<Monitor> monitorOptional = monitorDao.findMonitorByNameEquals(monitor.getName()); + if (monitorOptional.isPresent()) { + Monitor existMonitor = monitorOptional.get(); + if (isModify) { + if (!existMonitor.getId().equals(monitor.getId())) { + throw new IllegalArgumentException("Monitoring name already exists!"); + } + } else { + throw new IllegalArgumentException("Monitoring name already exists!"); + } + } + } + // the dispatch collector must exist if pin + if (StringUtils.hasText(monitorDto.getCollector())) { + Optional<Collector> optionalCollector = collectorDao.findCollectorByName(monitorDto.getCollector()); + if (optionalCollector.isEmpty()) { + throw new IllegalArgumentException("The pinned collector does not exist."); + } + } else { + monitorDto.setCollector(null); + } + // Parameter definition structure verification + List<ParamDefineInfo> paramDefines = appService.getAppParamDefines(monitor.getApp()); + if (!CollectionUtils.isEmpty(paramDefines)) { + boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) + || !StringUtils.hasText(monitor.getScrape()); + for (ParamDefineInfo paramDefine : paramDefines) { + String field = paramDefine.getField(); + MonitorParam param = paramMap.get(field); + // Get the host from service discovery + if (!isStatic && "host".equals(field)) { + continue; + } + if (paramDefine.isRequired() && (param == null || param.getParamValue() == null)) { + throw new IllegalArgumentException("Params field " + field + " is required."); + } + if (param != null && StringUtils.hasText(param.getParamValue())) { + paramValidatorManager.validate(paramDefine, param); + } + } + } + checkJobFields(monitorDto.getMonitor().getApp()); + } + + private void checkJobFields(String app) { + if (null == app || app.trim().isEmpty()) { + return; + } + Job job = appService.getAppDefine(app); + if (null != job && !CollectionUtils.isEmpty(job.getMetrics())) { + for (Metrics metrics : job.getMetrics()) { + if (null == metrics.getFields() || metrics.getFields().isEmpty()) { + continue; + } + for (Metrics.Field field : metrics.getFields()) { + if (JexlCheckerUtil.verifyKeywords(field.getField())) { + throw new IllegalArgumentException(job.getApp() + " " + metrics.getName() + " " + + field.getField() + " prohibited keywords, please modify the template information."); + } + if (JexlCheckerUtil.verifyStartCharacter(field.getField())) { + throw new IllegalArgumentException(job.getApp() + " " + metrics.getName() + " " + + field.getField() + + " illegal start character, please modify the template information."); + } + if (JexlCheckerUtil.verifySpaces(field.getField())) { + throw new IllegalArgumentException(job.getApp() + " " + metrics.getName() + " " + + field.getField() + " no spaces allowed, please modify the template information."); + } + } + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void modifyMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard grafanaDashboard) + throws RuntimeException { + long monitorId = monitor.getId(); + // Check to determine whether the monitor corresponding to the monitor id exists + Optional<Monitor> queryOption = monitorDao.findById(monitorId); + if (queryOption.isEmpty()) { + throw new IllegalArgumentException("The Monitor " + monitorId + " not exists"); + } + Monitor preMonitor = queryOption.get(); + if (!preMonitor.getApp().equals(monitor.getApp())) { + // The type of monitoring cannot be modified + throw new IllegalArgumentException("Can not modify monitor's app type"); + } + Map<String, String> labels = monitor.getLabels(); + if (labels == null) { + labels = new HashMap<>(8); + monitor.setLabels(labels); + } + + List<Label> addLabels = labelService.determineNewLabels(labels.entrySet()); + + if (!addLabels.isEmpty()) { + labelDao.saveAll(addLabels); + } + + String instance = monitor.getInstance(); + // The port field may be null + Param portParam = params.stream() + .filter(param -> PARAM_FIELD_PORT.equals(param.getField())) + .findFirst() + .orElse(null); + String portWithMark = (Objects.isNull(portParam) || !StringUtils.hasText(portParam.getParamValue())) + ? "" + : SignConstants.DOUBLE_MARK + portParam.getParamValue(); + if (Objects.nonNull(instance)) { + instance = instance + portWithMark; + } + monitor.setInstance(instance); + + boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) + || !StringUtils.hasText(monitor.getScrape()); + if (preMonitor.getStatus() != CommonConstants.MONITOR_PAUSED_CODE) { + // Construct the collection task Job entity + String app = isStatic ? monitor.getApp() : monitor.getScrape(); + Job appDefine = appService.getAppDefine(app); + if (!isStatic) { + appDefine.setSd(true); + } + if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) { + appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName()); + } + appDefine.setId(preMonitor.getJobId()); + appDefine.setMonitorId(monitorId); + appDefine.setDefaultInterval(monitor.getIntervals()); + appDefine.setCyclic(true); + appDefine.setTimestamp(System.currentTimeMillis()); + appDefine.setScheduleType(monitor.getScheduleType()); + appDefine.setCronExpression(monitor.getCronExpression()); + Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(), + CommonConstants.LABEL_INSTANCE, monitor.getInstance()); + appDefine.setMetadata(metadata); + appDefine.setLabels(monitor.getLabels()); + appDefine.setAnnotations(monitor.getAnnotations()); + List<Configmap> configmaps = params.stream() + .map(param -> new Configmap(param.getField(), param.getParamValue(), param.getType())) + .collect(Collectors.toList()); + appDefine.setConfigmap(configmaps); + long newJobId; + if (collector == null) { + newJobId = collectJobScheduling.updateAsyncCollectJob(appDefine); + } else { + newJobId = collectJobScheduling.updateAsyncCollectJob(appDefine, collector); + } + monitor.setJobId(newJobId); + } + + // After the update is successfully released, refresh the database + try { + collectorMonitorBindDao.deleteCollectorMonitorBindsByMonitorId(monitorId); + if (collector != null) { + CollectorMonitorBind collectorMonitorBind = CollectorMonitorBind.builder() + .collector(collector).monitorId(monitorId) + .build(); + collectorMonitorBindDao.save(collectorMonitorBind); + } + // force update gmtUpdate time, due the case: monitor not change, param change. + // we also think monitor change + monitor.setGmtUpdate(LocalDateTime.now()); + // preserve the live status owned by the real-time collection path; the modify request must not overwrite it + monitor.setStatus(preMonitor.getStatus()); + // update or open grafana dashboard + if (monitor.getApp().equals(CommonConstants.PROMETHEUS) && grafanaDashboard != null) { + if (grafanaDashboard.isEnabled()) { + dashboardService.createOrUpdateDashboard(grafanaDashboard.getTemplate(), monitorId); + } else { + dashboardService.closeGrafanaDashboard(monitorId); + } + } + monitorDao.save(monitor); + paramDao.saveAll(params); + } catch (Exception e) { + log.error(e.getMessage(), e); + // Repository brushing abnormally cancels the previously delivered task + collectJobScheduling.cancelAsyncCollectJob(monitor.getJobId()); + throw new MonitorDatabaseException(e.getMessage()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteMonitor(long id) throws RuntimeException { + deleteMonitors(Sets.newHashSet(id)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteMonitors(Set<Long> ids) throws RuntimeException { + if (CollectionUtils.isEmpty(ids)) { + return; + } + Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId) + .collect(Collectors.toSet()); + Set<Long> allMonitorIds = new HashSet<>(ids); + allMonitorIds.addAll(subMonitorIds); + List<Monitor> monitors = monitorDao.findMonitorsByIdIn(allMonitorIds); + if (!monitors.isEmpty()) { + monitorDao.deleteAll(monitors); + paramDao.deleteParamsByMonitorIdIn(ids); + Set<Long> monitorIds = monitors.stream().map(Monitor::getId).collect(Collectors.toSet()); + alertDefineBindDao.deleteAlertDefineMonitorBindsByMonitorIdIn(monitorIds); + monitorBindDao.deleteMonitorBindByBizIdIn(monitorIds); + metricsFavoriteService.deleteFavoritesByMonitorIdIn(monitorIds); + for (Monitor monitor : monitors) { + monitorBindDao.deleteByMonitorId(monitor.getId()); + collectorMonitorBindDao.deleteCollectorMonitorBindsByMonitorId(monitor.getId()); + collectJobScheduling.cancelAsyncCollectJob(monitor.getJobId()); + applicationContext.publishEvent(new MonitorDeletedEvent(applicationContext, monitor.getId())); + } + } + } + + @Override + @Transactional(readOnly = true) + public MonitorDto getMonitorDto(long id) throws RuntimeException { + Optional<Monitor> monitorOptional = monitorDao.findById(id); + if (monitorOptional.isPresent()) { + // Get current user ID for favorite status + String currentUserId = null; + try { + SubjectSum subjectSum = SurenessContextHolder.getBindSubject(); + currentUserId = String.valueOf(subjectSum.getPrincipal()); + } catch (Exception e) { + log.debug("No user context found, favorites will be disabled"); + } + Set<String> favoritedMetrics = metricsFavoriteService.getUserFavoritedMetrics(currentUserId, id); + + Monitor monitor = monitorOptional.get(); + MonitorDto monitorDto = new MonitorDto(); + List<Param> params = paramDao.findParamsByMonitorId(id); + monitorDto.setParams(params); + List<MetricsInfo> metricsInfos; + if (DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(monitor.getApp()) + || monitor.getType() == CommonConstants.MONITOR_TYPE_PUSH_AUTO_CREATE) { + List<CollectRep.MetricsData> metricsDataList = warehouseService.queryMonitorMetricsData(id); + metricsInfos = metricsDataList.stream() + .map(t -> MetricsInfo.builder().name(t.getMetrics()) + .favorited(favoritedMetrics.contains(t.getMetrics())).build()) + .collect(Collectors.toList()); + monitorDto.setGrafanaDashboard(dashboardService.getDashboardByMonitorId(id)); + } else { + boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) + || !StringUtils.hasText(monitor.getScrape()); + String type = isStatic ? monitor.getApp() : monitor.getScrape(); + Job job = appService.getAppDefine(type); + metricsInfos = job.getMetrics().stream() + .filter(Metrics::isVisible) + .map(t -> MetricsInfo.builder().name(t.getName()) + .favorited(favoritedMetrics.contains(t.getName())).build()) + .collect(Collectors.toList()); + } + monitorDto.setMetrics(metricsInfos); + monitorDto.setMonitor(monitor); + Optional<CollectorMonitorBind> bindOptional = collectorMonitorBindDao + .findCollectorMonitorBindByMonitorId(monitor.getId()); + bindOptional.ifPresent(bind -> monitorDto.setCollector(bind.getCollector())); + return monitorDto; + } else { + return null; + } + } + + @Override + public Page<Monitor> getMonitors(List<Long> monitorIds, String app, String search, Byte status, String sort, + String order, int pageIndex, int pageSize, String labels) { + Specification<Monitor> specification = (root, query, criteriaBuilder) -> { + List<Predicate> andList = new ArrayList<>(); + if (!CollectionUtils.isEmpty(monitorIds)) { + CriteriaBuilder.In<Long> inPredicate = criteriaBuilder.in(root.get("id")); + for (long id : monitorIds) { + inPredicate.value(id); + } + andList.add(inPredicate); + } + if (StringUtils.hasText(app)) { + Predicate predicateApp = criteriaBuilder.equal(root.get("app"), app); + andList.add(predicateApp); + } + if (status != null && status >= 0 && status < ALL_MONITOR_STATUS) { + Predicate predicateStatus = criteriaBuilder.equal(root.get("status"), status); + andList.add(predicateStatus); + } + Predicate[] andPredicates = new Predicate[andList.size()]; + Predicate andPredicate = criteriaBuilder.and(andList.toArray(andPredicates)); + + List<Predicate> orList = new ArrayList<>(); + if (StringUtils.hasText(search)) { + Predicate predicateHost = criteriaBuilder.like(root.get("instance"), "%" + search + "%"); + Predicate predicateName = criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), + "%" + search.toLowerCase() + "%"); + Long id = Longs.tryParse(search); + if (id != null) { + orList.add(criteriaBuilder.equal(root.get("id"), id)); + } + orList.add(predicateHost); + orList.add(predicateName); + } + if (StringUtils.hasText(labels)) { + String[] labelAres = labels.split(","); + for (String label : labelAres) { + String[] labelArr = label.split(":"); + String labelName = labelArr[0]; + String labelValue = labelArr.length == 2 ? labelArr[1] : null; + // create every label condition + if (labelValue == null) { + orList.add(criteriaBuilder.like(root.get("labels"), "%" + labelName + "%")); + } else { + String pattern = String.format("%%\"%s\":\"%s\"%%", labelName, labelValue); + orList.add(criteriaBuilder.like(root.get("labels"), pattern)); + } + } + } + Predicate[] orPredicates = new Predicate[orList.size()]; + Predicate orPredicate = criteriaBuilder.or(orList.toArray(orPredicates)); + + if (andPredicates.length == 0 && orPredicates.length == 0) { + return query.where().getRestriction(); + } else if (andPredicates.length == 0) { + return orPredicate; + } else if (orPredicates.length == 0) { + return andPredicate; + } else { + return query.where(andPredicate, orPredicate).getRestriction(); + } + }; + // Pagination is a must + // Handle null sort/order parameters with defaults + String effectiveSort = (sort == null || sort.isEmpty()) ? "id" : sort; + String effectiveOrder = (order == null || order.isEmpty()) ? "desc" : order; + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(effectiveOrder), effectiveSort)); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); + return monitorDao.findAll(specification, pageRequest); + } + + @Override + public void cancelManageMonitors(Set<Long> ids) { + if (CollectionUtils.isEmpty(ids)) { + return; + } + // Update monitoring status Delete corresponding monitoring periodic task + // The jobId is not deleted, and the jobId is reused again after the management + // is started. + Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId) + .collect(Collectors.toSet()); + ids.addAll(subMonitorIds); + List<Monitor> managedMonitors = monitorDao.findMonitorsByIdIn(ids) + .stream().filter(monitor -> monitor.getStatus() != CommonConstants.MONITOR_PAUSED_CODE) + .peek(monitor -> monitor.setStatus(CommonConstants.MONITOR_PAUSED_CODE)) + .collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(managedMonitors)) { + for (Monitor monitor : managedMonitors) { + collectJobScheduling.cancelAsyncCollectJob(monitor.getJobId()); + } + monitorDao.saveAll(managedMonitors); + } + } + + @Override + public void enableManageMonitors(Set<Long> ids) { + // Update monitoring status Add corresponding monitoring periodic task + Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId) + .collect(Collectors.toSet()); + ids.addAll(subMonitorIds); + List<Monitor> unManagedMonitors = monitorDao.findMonitorsByIdIn(ids) + .stream().filter(monitor -> monitor.getStatus() == CommonConstants.MONITOR_PAUSED_CODE) + .peek(monitor -> monitor.setStatus(CommonConstants.MONITOR_UP_CODE)) + .collect(Collectors.toList()); + if (unManagedMonitors.isEmpty()) { + return; + } + + for (Monitor monitor : unManagedMonitors) { + // Construct the collection task Job entity + List<Param> params = paramDao.findParamsByMonitorId(monitor.getId()); + boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) + || !StringUtils.hasText(monitor.getScrape()); + String app = isStatic ? monitor.getApp() : monitor.getScrape(); + Job appDefine = appService.getAppDefine(app); + if (!isStatic) { + appDefine.setSd(true); + } + if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) { + appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName()); + } + appDefine.setMonitorId(monitor.getId()); + appDefine.setDefaultInterval(monitor.getIntervals()); + appDefine.setCyclic(true); + appDefine.setTimestamp(System.currentTimeMillis()); + appDefine.setScheduleType(monitor.getScheduleType()); + appDefine.setCronExpression(monitor.getCronExpression()); + Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(), + CommonConstants.LABEL_INSTANCE, monitor.getInstance()); + appDefine.setMetadata(metadata); + appDefine.setLabels(monitor.getLabels()); + appDefine.setAnnotations(monitor.getAnnotations()); + List<Configmap> configmaps = params.stream() + .map(param -> new Configmap(param.getField(), param.getParamValue(), param.getType())) + .collect(Collectors.toList()); + List<RuntimeParamDefine> paramDefaultValue = appDefine.getParams().stream() + .filter(item -> StringUtils.hasText(item.getDefaultValue())) + .toList(); + paramDefaultValue.forEach(defaultVar -> { + if (configmaps.stream().noneMatch(item -> item.getKey().equals(defaultVar.getField()))) { + Configmap configmap = new Configmap(defaultVar.getField(), defaultVar.getDefaultValue(), + CommonConstants.TYPE_STRING); + configmaps.add(configmap); + } + }); + appDefine.setConfigmap(configmaps); + + // Issue collection tasks + Optional<CollectorMonitorBind> bindOptional = collectorMonitorBindDao + .findCollectorMonitorBindByMonitorId(monitor.getId()); + String collector = bindOptional.map(CollectorMonitorBind::getCollector).orElse(null); + long newJobId = collectJobScheduling.addAsyncCollectJob(appDefine, collector); + monitor.setJobId(newJobId); + applicationContext.publishEvent(new MonitorDeletedEvent(applicationContext, monitor.getId())); + try { + detectMonitor(monitor, params, collector); + } catch (Exception ignored) { + } + } + monitorDao.saveAll(unManagedMonitors); + } + + @Override + public List<AppCount> getAllAppMonitorsCount() { + List<AppCount> appCounts = monitorDao.findAppsStatusCount(); + if (CollectionUtils.isEmpty(appCounts)) { + return null; + } + // Statistical category information, calculate the number of corresponding + // states for each monitor + Map<String, AppCount> appCountMap = new HashMap<>(appCounts.size()); + for (AppCount item : appCounts) { + AppCount appCount = appCountMap.getOrDefault(item.getApp(), new AppCount()); + appCount.setApp(item.getApp()); + switch (item.getStatus()) { + case CommonConstants.MONITOR_UP_CODE -> + appCount.setAvailableSize(appCount.getAvailableSize() + item.getSize()); + case CommonConstants.MONITOR_DOWN_CODE -> + appCount.setUnAvailableSize(appCount.getUnAvailableSize() + item.getSize()); + case CommonConstants.MONITOR_PAUSED_CODE -> + appCount.setUnManageSize(appCount.getUnManageSize() + item.getSize()); + default -> { + } + } + appCountMap.put(item.getApp(), appCount); + } + // Traverse the map obtained by statistics and convert it into a List<App Count> + // result set + return appCountMap.values().stream().map(item -> { + item.setSize(item.getAvailableSize() + item.getUnManageSize() + item.getUnAvailableSize()); + try { + Job job = appService.getAppDefine(item.getApp()); + item.setCategory(job.getCategory()); + } catch (Exception ignored) { + return null; + } + return item; + }).filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + public void updateAppCollectJob(Job job) { + List<Monitor> monitors = monitorDao.findMonitorsByAppEquals(job.getApp()) + .stream().filter(monitor -> monitor.getStatus() != CommonConstants.MONITOR_PAUSED_CODE) + .toList(); + if (monitors.isEmpty()) { + return; + } + List<CollectorMonitorBind> monitorBinds = collectorMonitorBindDao.findCollectorMonitorBindsByMonitorIdIn( + monitors.stream().map(Monitor::getId).collect(Collectors.toSet())); + Map<Long, String> monitorIdCollectorMap = monitorBinds.stream().collect( + Collectors.toMap(CollectorMonitorBind::getMonitorId, CollectorMonitorBind::getCollector)); + for (Monitor monitor : monitors) { + try { + Job appDefine = job.clone(); + if (monitor == null || appDefine == null || monitor.getId() == null || monitor.getJobId() == null) { + log.error("update monitor job error when template modify, define | id | jobId is null. continue"); + continue; + } + if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) { + appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName()); + } + appDefine.setId(monitor.getJobId()); + appDefine.setMonitorId(monitor.getId()); + appDefine.setDefaultInterval(monitor.getIntervals()); + appDefine.setCyclic(true); + appDefine.setTimestamp(System.currentTimeMillis()); + appDefine.setScheduleType(monitor.getScheduleType()); + appDefine.setCronExpression(monitor.getCronExpression()); + Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(), + CommonConstants.LABEL_INSTANCE, monitor.getInstance()); + appDefine.setMetadata(metadata); + appDefine.setLabels(monitor.getLabels()); + appDefine.setAnnotations(monitor.getAnnotations()); + List<Param> params = paramDao.findParamsByMonitorId(monitor.getId()); + List<Configmap> configmaps = params.stream().map(param -> new Configmap(param.getField(), + param.getParamValue(), param.getType())).collect(Collectors.toList()); + List<RuntimeParamDefine> paramDefaultValue = appDefine.getParams().stream() + .filter(item -> StringUtils.hasText(item.getDefaultValue())) + .toList(); + paramDefaultValue.forEach(defaultVar -> { + if (configmaps.stream().noneMatch(item -> item.getKey().equals(defaultVar.getField()))) { + Configmap configmap = new Configmap(defaultVar.getField(), defaultVar.getDefaultValue(), + (byte) 1); + configmaps.add(configmap); + } + }); + appDefine.setConfigmap(configmaps); + // if is pinned collector + String collector = monitorIdCollectorMap.get(monitor.getId()); + // Delivering a collection task + long newJobId = collectJobScheduling.updateAsyncCollectJob(appDefine, collector); + monitor.setJobId(newJobId); + monitorDao.save(monitor); + } catch (Exception e) { + log.error("update monitor job error when template modify: {}.continue", e.getMessage(), e); + } + } + } + + @Override + public Monitor getMonitor(Long monitorId) { + return monitorDao.findById(monitorId).orElse(null); + } + + @Override + public void updateMonitorStatus(Long monitorId, byte status) { + monitorDao.updateMonitorStatus(monitorId, status); + } + + @Override + public List<Monitor> getAppMonitors(String app) { + return monitorDao.findMonitorsByAppEquals(app); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void copyMonitor(Long id) { + // Get the source monitor information + Optional<Monitor> monitorOptional = monitorDao.findById(id); + if (monitorOptional.isEmpty()) { + throw new IllegalArgumentException("Monitor not found: " + id); + } + Monitor sourceMonitor = monitorOptional.get(); + // Get the parameters of source monitor + List<Param> sourceParams = paramDao.findParamsByMonitorId(id); + // Create new monitor object + Monitor newMonitor = new Monitor(); + // Copy basic properties, exclude ID, jobId and status + BeanUtils.copyProperties(sourceMonitor, newMonitor, "id", "jobId", "status"); + // Set new name + newMonitor.setName(sourceMonitor.getName() + "_copy"); + // Set initial status + newMonitor.setStatus(CommonConstants.MONITOR_UP_CODE); + // Set create and update time + newMonitor.setGmtCreate(LocalDateTime.now()); + newMonitor.setGmtUpdate(LocalDateTime.now()); + // Copy parameters + List<Param> newParams = new ArrayList<>(); + if (!sourceParams.isEmpty()) { + for (Param sourceParam : sourceParams) { + Param newParam = new Param(); + BeanUtils.copyProperties(sourceParam, newParam, "id"); + newParam.setMonitorId(newMonitor.getId()); + newParams.add(newParam); + } + } + addMonitor(newMonitor, newParams, null, null); + } + + private void detectSdMonitor(Monitor monitor, List<Param> params, String collector) { + Long monitorId = monitor.getId(); + if (monitorId == null || monitorId == 0) { + monitorId = MONITOR_ID_TMP; + } + if (monitor.getScrape() == null || params == null || params.isEmpty()) { + throw new IllegalArgumentException("scrape params is null or empty!"); + } + Job appDefine = appService.getAppDefine(monitor.getScrape()); + appDefine.setMonitorId(monitorId); + appDefine.setCyclic(false); + appDefine.setTimestamp(System.currentTimeMillis()); + Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName()); + appDefine.setMetadata(metadata); + appDefine.setLabels(monitor.getLabels()); + appDefine.setAnnotations(monitor.getAnnotations()); + List<Configmap> configmaps = params.stream() + .map(param -> new Configmap(param.getField(), param.getParamValue(), param.getType())) + .collect(Collectors.toList()); + appDefine.setConfigmap(configmaps); + appDefine.setSd(true); + List<CollectRep.MetricsData> collectRep; + if (collector != null) { + collectRep = collectJobScheduling.collectSyncJobData(appDefine, collector); + } else { + collectRep = collectJobScheduling.collectSyncJobData(appDefine); + } + monitor.setStatus(CommonConstants.MONITOR_UP_CODE); + // If the detection result fails, a detection exception is thrown + if (collectRep == null || collectRep.isEmpty()) { + monitor.setStatus(CommonConstants.MONITOR_DOWN_CODE); + throw new MonitorDetectException("Collect Timeout No Response"); + } + if (collectRep.get(0).getCode() != CollectRep.Code.SUCCESS) { + monitor.setStatus(CommonConstants.MONITOR_DOWN_CODE); + throw new MonitorDetectException(collectRep.get(0).getMsg()); + } + collectRep.forEach(CollectRep.MetricsData::close); + } + + private void detectMonitorDirectly(Monitor monitor, List<Param> params, String collector) { + Long monitorId = monitor.getId(); + if (monitorId == null || monitorId == 0) { + monitorId = MONITOR_ID_TMP; + } + Job appDefine = appService.getAppDefine(monitor.getApp()); + if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) { + appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName()); + } + appDefine.setMonitorId(monitorId); + appDefine.setCyclic(false); + appDefine.setTimestamp(System.currentTimeMillis()); + Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(), + CommonConstants.LABEL_INSTANCE, monitor.getInstance()); + appDefine.setMetadata(metadata); + appDefine.setLabels(monitor.getLabels()); + appDefine.setAnnotations(monitor.getAnnotations()); + List<Configmap> configmaps = params.stream() + .map(param -> new Configmap(param.getField(), param.getParamValue(), param.getType())) + .collect(Collectors.toList()); + appDefine.setConfigmap(configmaps); + // To detect availability, you only need to collect the set of availability + // metrics with a priority of 0. + List<Metrics> availableMetrics = appDefine.getMetrics().stream() + .filter(item -> item.getPriority() == 0).collect(Collectors.toList()); + appDefine.setMetrics(availableMetrics); + List<CollectRep.MetricsData> collectRep; + if (collector != null) { + collectRep = collectJobScheduling.collectSyncJobData(appDefine, collector); + } else { + collectRep = collectJobScheduling.collectSyncJobData(appDefine); + } + monitor.setStatus(CommonConstants.MONITOR_UP_CODE); + + // If the detection result fails, a detection exception is thrown + if (collectRep == null || collectRep.isEmpty()) { + monitor.setStatus(CommonConstants.MONITOR_DOWN_CODE); + throw new MonitorDetectException("Collect Timeout No Response"); + } + if (collectRep.get(0).getCode() != CollectRep.Code.SUCCESS) { + monitor.setStatus(CommonConstants.MONITOR_DOWN_CODE); + throw new MonitorDetectException(collectRep.get(0).getMsg()); + } + collectRep.forEach(CollectRep.MetricsData::close); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MuteGeneralConfigServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MuteGeneralConfigServiceImpl.java new file mode 100644 index 0000000..c96b973 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MuteGeneralConfigServiceImpl.java @@ -0,0 +1,77 @@ +/* + * 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.manager.service.impl; + +import tools.jackson.core.type.TypeReference; +import java.lang.reflect.Type; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum; +import org.apache.hertzbeat.manager.pojo.dto.MuteConfig; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; + +/** + * mute config service + */ +@Order(Ordered.HIGHEST_PRECEDENCE) +@Service +@Slf4j +public class MuteGeneralConfigServiceImpl extends AbstractGeneralConfigServiceImpl<MuteConfig> { + + protected MuteGeneralConfigServiceImpl(GeneralConfigDao generalConfigDao) { + super(generalConfigDao); + } + + /** + * <p>Get TypeReference object of configuration type.</p> + * + * @return TypeReference object + */ + @Override + public TypeReference<MuteConfig> getTypeReference() { + return new TypeReference<>() { + @Override + public Type getType() { + return MuteConfig.class; + } + }; + } + + /** + * config type: email, sms + * + * @return type string + */ + @Override + public String type() { + return GeneralConfigTypeEnum.mute.name(); + } + + /** + * handler after save config + * + * @param config config + */ + @Override + public void handler(MuteConfig config) { + log.info("handler mute config: {}", config); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ObjectStoreConfigServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ObjectStoreConfigServiceImpl.java new file mode 100644 index 0000000..e4d1a14 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ObjectStoreConfigServiceImpl.java @@ -0,0 +1,132 @@ +/* + * 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.manager.service.impl; + +import tools.jackson.core.type.TypeReference; +import com.obs.services.ObsClient; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.pojo.dto.ObjectStoreConfigChangeEvent; +import org.apache.hertzbeat.manager.pojo.dto.ObjectStoreDTO; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import jakarta.annotation.Resource; +import java.lang.reflect.Type; +import java.net.URL; + +/** + * File storage configuration service + */ +@Order(Ordered.HIGHEST_PRECEDENCE) +@Slf4j +@Service +public class ObjectStoreConfigServiceImpl extends + AbstractGeneralConfigServiceImpl<ObjectStoreDTO<ObjectStoreDTO.ObsConfig>> implements InitializingBean { + + private static final String BEAN_NAME = "ObjectStoreService"; + @Resource + private DefaultListableBeanFactory beanFactory; + @Resource + private ApplicationContext ctx; + + public ObjectStoreConfigServiceImpl(GeneralConfigDao generalConfigDao) { + super(generalConfigDao); + } + + @Override + public String type() { + return GeneralConfigTypeEnum.oss.name(); + } + + @Override + public TypeReference<ObjectStoreDTO<ObjectStoreDTO.ObsConfig>> getTypeReference() { + return new TypeReference<>() { + @Override + public Type getType() { + return ObjectStoreDTO.class; + } + }; + } + + @Override + public void handler(ObjectStoreDTO<ObjectStoreDTO.ObsConfig> config) { + // initialize file storage service + if (config != null) { + if (config.getType() == ObjectStoreDTO.Type.OBS) { + initObs(config); + // case other object store service + } + ctx.publishEvent(new ObjectStoreConfigChangeEvent(config)); + } + log.warn("object store config is null, please check the configuration file."); + } + + /** + * init Huawei Cloud OBS + */ + private void initObs(ObjectStoreDTO<ObjectStoreDTO.ObsConfig> config) { + var obsConfig = JsonUtil.convertValue(config.getConfig(), ObjectStoreDTO.ObsConfig.class); + Assert.hasText(obsConfig.getAccessKey(), "cannot find obs accessKey"); + Assert.hasText(obsConfig.getSecretKey(), "cannot find obs secretKey"); + Assert.hasText(obsConfig.getEndpoint(), "cannot find obs endpoint"); + Assert.hasText(obsConfig.getBucketName(), "cannot find obs bucket name"); + + // Add domain name verification for Huawei Cloud OBS endpoint + validateObsEndpoint(obsConfig.getEndpoint()); + + var obsClient = new ObsClient(obsConfig.getAccessKey(), obsConfig.getSecretKey(), obsConfig.getEndpoint()); + + beanFactory.destroySingleton(BEAN_NAME); + beanFactory.registerSingleton(BEAN_NAME, new ObsObjectStoreServiceImpl(obsClient, obsConfig.getBucketName(), obsConfig.getSavePath())); + + log.info("obs store service init success."); + } + + /** + * Verify Huawei Cloud OBS endpoint domain name + * Only myhuaweicloud.com domain name is allowed + * Refer: <a href="https://console-intl.huaweicloud.com/apiexplorer/#/endpoint">...</a> + */ + public void validateObsEndpoint(String endpoint) { + try { + URL url = new URL(endpoint); + String host = url.getHost(); + + // Verify whether it is a Huawei Cloud domain name + if (!host.endsWith(".myhuaweicloud.com")) { + throw new IllegalArgumentException("Invalid OBS endpoint domain. Only myhuaweicloud.com is allowed"); + } + } catch (Exception e) { + throw new IllegalArgumentException("Invalid OBS endpoint: " + e.getMessage()); + } + } + + @Override + public void afterPropertiesSet() throws Exception { + // init file storage + handler(getConfig()); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ObsObjectStoreServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ObsObjectStoreServiceImpl.java new file mode 100644 index 0000000..4d839d1 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ObsObjectStoreServiceImpl.java @@ -0,0 +1,100 @@ +/* + * 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.manager.service.impl; + +import com.obs.services.ObsClient; +import com.obs.services.model.ListObjectsRequest; +import java.io.InputStream; +import java.util.List; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.SignConstants; +import org.apache.hertzbeat.manager.pojo.dto.FileDTO; +import org.apache.hertzbeat.manager.pojo.dto.ObjectStoreDTO; +import org.apache.hertzbeat.manager.service.ObjectStoreService; + +/** + * Huawei cloud storage service + */ +@Slf4j +public class ObsObjectStoreServiceImpl implements ObjectStoreService { + private final ObsClient obsClient; + private final String bucketName; + private final String rootPath; + + public ObsObjectStoreServiceImpl(ObsClient obsClient, String bucketName, String rootPath) { + this.obsClient = obsClient; + this.bucketName = bucketName; + if (rootPath.startsWith(SignConstants.RIGHT_DASH)) { + this.rootPath = rootPath.substring(1); + } else { + this.rootPath = rootPath; + } + } + + @Override + public boolean upload(String filePath, InputStream is) { + var objectKey = getObjectKey(filePath); + var response = obsClient.putObject(bucketName, objectKey, is); + return Objects.equals(response.getStatusCode(), 200); + } + + @Override + public void remove(String filePath) { + var objectKey = getObjectKey(filePath); + obsClient.deleteObject(bucketName, objectKey); + } + + @Override + public boolean isExist(String filePath) { + var objectKey = getObjectKey(filePath); + return obsClient.doesObjectExist(bucketName, objectKey); + } + + @Override + public FileDTO download(String filePath) { + var objectKey = getObjectKey(filePath); + try { + var obsObject = obsClient.getObject(bucketName, objectKey); + return new FileDTO(filePath, obsObject.getObjectContent()); + } catch (Exception ex) { + log.warn("download file from obs error {}", objectKey); + return null; + } + } + + @Override + public List<FileDTO> list(String dir) { + var request = new ListObjectsRequest(bucketName); + request.setPrefix(getObjectKey(dir)); + return obsClient.listObjects(request).getObjects() + .stream() + .map(it -> new FileDTO(it.getObjectKey(), it.getObjectContent())) + .toList(); + } + + @Override + public ObjectStoreDTO.Type type() { + return ObjectStoreDTO.Type.OBS; + } + + private String getObjectKey(String filePath) { + return rootPath + SignConstants.RIGHT_DASH + filePath; + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/PluginServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/PluginServiceImpl.java new file mode 100644 index 0000000..a36f453 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/PluginServiceImpl.java @@ -0,0 +1,566 @@ +/* + * 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.manager.service.impl; + +import jakarta.annotation.PostConstruct; +import jakarta.persistence.criteria.Predicate; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.io.FileUtils; +import org.apache.hertzbeat.common.constants.PluginType; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.manager.PluginItem; +import org.apache.hertzbeat.common.entity.manager.PluginMetadata; +import org.apache.hertzbeat.common.entity.plugin.PluginConfig; +import org.apache.hertzbeat.common.entity.plugin.PluginContext; +import org.apache.hertzbeat.common.support.exception.CommonException; +import org.apache.hertzbeat.manager.dao.PluginItemDao; +import org.apache.hertzbeat.manager.dao.PluginMetadataDao; +import org.apache.hertzbeat.manager.dao.PluginParamDao; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.apache.hertzbeat.manager.pojo.dto.PluginUpload; +import org.apache.hertzbeat.manager.pojo.dto.PluginParam; +import org.apache.hertzbeat.manager.pojo.dto.PluginParametersVO; +import org.apache.hertzbeat.manager.service.PluginService; +import org.apache.hertzbeat.plugin.PostAlertPlugin; +import org.apache.hertzbeat.plugin.Plugin; +import org.apache.hertzbeat.plugin.PostCollectPlugin; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.error.YAMLException; + +/** + * plugin service + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PluginServiceImpl implements PluginService { + + private final PluginMetadataDao metadataDao; + + private final PluginItemDao itemDao; + + private final PluginParamDao pluginParamDao; + + public static Map<Class<?>, PluginType> PLUGIN_TYPE_MAPPING = new HashMap<>(); + + /** + * plugin status + */ + private static final Map<String, Boolean> PLUGIN_ENABLE_STATUS = new ConcurrentHashMap<>(); + + /** + * plugin param define + */ + private static final Map<Long, PluginConfig> PARAMS_CONFIG_MAP = new ConcurrentHashMap<>(); + + /** + * plugin params + */ + private static final Map<Long, List<Configmap>> PARAMS_MAP = new ConcurrentHashMap<>(); + + /** + * pluginItem Mapping pluginId + */ + private static final Map<String, Long> ITEM_TO_PLUGINMETADATAID_MAP = new ConcurrentHashMap<>(); + + private final List<URLClassLoader> pluginClassLoaders = new ArrayList<>(); + + @Override + @Transactional + public void deletePlugins(Set<Long> ids) { + List<PluginMetadata> plugins = metadataDao.findAllById(ids); + // disable the plugins that need to be removed + for (PluginMetadata plugin : plugins) { + plugin.setEnableStatus(false); + updateStatus(plugin); + } + // reload classloader + loadJarToClassLoader(); + for (PluginMetadata plugin : plugins) { + try { + // delete jar file + File jarFile = new File(plugin.getJarFilePath()); + if (jarFile.exists()) { + FileUtils.delete(jarFile); + } + // removing jar files that are dependencies for the plugin + File otherLibDir = new File(getOtherLibDir(plugin.getJarFilePath())); + if (otherLibDir.exists()) { + FileUtils.deleteDirectory(otherLibDir); + } + // delete metadata + metadataDao.deleteById(plugin.getId()); + syncPluginParamMap(plugin.getId(), null, true); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + pluginParamDao.deletePluginParamsByPluginMetadataIdIn(ids); + syncPluginStatus(); + + } + + /** + * get the directory where the JAR files dependent on the plugin are saved + * + * @param pluginJarPath jar file path + * @return lib dir + */ + private String getOtherLibDir(String pluginJarPath) { + return pluginJarPath.substring(0, pluginJarPath.lastIndexOf(".")); + } + + @Override + public void updateStatus(PluginMetadata plugin) { + Optional<PluginMetadata> pluginMetadata = metadataDao.findById(plugin.getId()); + if (pluginMetadata.isPresent()) { + PluginMetadata metadata = pluginMetadata.get(); + metadata.setEnableStatus(plugin.getEnableStatus()); + metadataDao.save(metadata); + syncSinglePluginStatus(metadata); + } else { + throw new IllegalArgumentException("The plugin is not existed"); + } + } + + @Override + public PluginParametersVO getParamDefine(Long pluginMetadataId) { + + PluginParametersVO pluginParametersVO = new PluginParametersVO(); + if (PARAMS_CONFIG_MAP.containsKey(pluginMetadataId)) { + PluginConfig config = PARAMS_CONFIG_MAP.get(pluginMetadataId); + List<PluginParam> paramsByPluginMetadataId = pluginParamDao.findParamsByPluginMetadataId(pluginMetadataId); + pluginParametersVO.setParamDefines(Optional.ofNullable(config).map(PluginConfig::getParams) + .orElse(new ArrayList<>()).stream().map(ParamDefineInfo::fromRuntime).toList()); + pluginParametersVO.setPluginParams(paramsByPluginMetadataId); + return pluginParametersVO; + } + return pluginParametersVO; + } + + @Override + @Transactional + public void savePluginParam(List<PluginParam> params) { + if (CollectionUtils.isEmpty(params)) { + return; + } + pluginParamDao.deletePluginParamsByPluginMetadataId(params.get(0).getPluginMetadataId()); + pluginParamDao.saveAll(params); + syncPluginParamMap(params.get(0).getPluginMetadataId(), params, false); + } + + private void syncPluginParamMap(Long pluginMetadataId, List<PluginParam> params, boolean isDelete) { + if (isDelete) { + PARAMS_MAP.remove(pluginMetadataId); + return; + } + List<Configmap> configmapList = params.stream().map(item -> new Configmap(item.getField(), item.getParamValue(), item.getType())).toList(); + PARAMS_MAP.put(pluginMetadataId, configmapList); + } + + static { + PLUGIN_TYPE_MAPPING.put(Plugin.class, PluginType.POST_ALERT); + PLUGIN_TYPE_MAPPING.put(PostAlertPlugin.class, PluginType.POST_ALERT); + PLUGIN_TYPE_MAPPING.put(PostCollectPlugin.class, PluginType.POST_COLLECT); + } + + /** + * verify the type of the jar package + * + * @param jarFile jar file + * @return return the result of jar package parsed + */ + public PluginMetadata validateJarFile(File jarFile, String extLibPath) { + PluginMetadata metadata = new PluginMetadata(); + List<PluginItem> pluginItems = new ArrayList<>(); + AtomicInteger pluginImplementationCount = new AtomicInteger(0); + try { + validateFilePath(jarFile, extLibPath); + URL jarUrl = new URL("file:" + jarFile.getAbsolutePath()); + validateJarUrl(jarUrl); + try (URLClassLoader classLoader = new URLClassLoader(new URL[]{jarUrl}, this.getClass().getClassLoader()); + JarFile jar = new JarFile(jarFile)) { + Enumeration<JarEntry> entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.getName().endsWith(".class")) { + String className = entry.getName().replace("/", ".").replace(".class", ""); + try { + Class<?> cls = classLoader.loadClass(className); + if (cls.isInterface()) { + continue; + } + if (pluginImplementationCount.get() >= 1) { + throw new CommonException("A plugin package can only contain one plugin implementation class"); + } + PLUGIN_TYPE_MAPPING.forEach((clazz, type) -> { + if (clazz.isAssignableFrom(cls)) { + pluginItems.add(new PluginItem(className, type)); + pluginImplementationCount.incrementAndGet(); + } + }); + } catch (ClassNotFoundException e) { + System.err.println("Failed to load class: " + className); + } + } + if ((entry.getName().contains("define")) && (entry.getName().endsWith(".yml") || entry.getName().endsWith(".yaml"))) { + PluginConfig config = readPluginConfig(jar, entry); + metadata.setParamCount(CollectionUtils.size(config.getParams())); + } + } + if (pluginItems.isEmpty()) { + throw new CommonException("Illegal plug-ins, please refer to https://hertzbeat.apache.org/docs/help/plugin/"); + } + } catch (IOException e) { + log.error("Error reading JAR file:{}", jarFile.getAbsoluteFile(), e); + throw new CommonException("Error reading JAR file: " + jarFile.getAbsolutePath()); + } + } catch (MalformedURLException e) { + log.error("Invalid JAR file URL: {}", jarFile.getAbsoluteFile(), e); + throw new CommonException("Invalid JAR file URL: " + jarFile.getAbsolutePath()); + } catch (YAMLException e) { + throw new CommonException("YAML the file format is incorrect"); + } + metadata.setItems(pluginItems); + return metadata; + } + + /** + * Validate that the file resides within the expected directory. + * + * @param file the file to validate + */ + private void validateFilePath(File file, String extLibPath) { + try { + String canonicalPath = file.getCanonicalPath(); + if (!canonicalPath.startsWith(extLibPath)) { + throw new CommonException("File is outside the allowed directory: " + canonicalPath); + } + } catch (IOException e) { + log.error("Error validating file path: {}", file.getAbsolutePath(), e); + throw new CommonException("Error validating file path: " + file.getAbsolutePath()); + } + } + + /** + * Validate that the URL uses the 'file:' protocol and does not point to an external resource. + * + * @param url the URL to validate + */ + private void validateJarUrl(URL url) { + if (!"file".equals(url.getProtocol())) { + throw new CommonException("Invalid URL protocol: " + url.getProtocol()); + } + } + + private void validateMetadata(PluginMetadata metadata) { + if (metadataDao.countPluginMetadataByName(metadata.getName()) != 0) { + throw new CommonException("A plugin named " + metadata.getName() + " already exists"); + } + } + + @Override + @SneakyThrows + @Transactional + public void savePlugin(PluginUpload pluginUpload) { + String jarPath = new File(this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()).getAbsolutePath(); + Path extLibPath = Paths.get(new File(jarPath).getParent(), "plugin-lib"); + File extLibDir = extLibPath.toFile(); + String fileName = pluginUpload.getJarFile().getOriginalFilename(); + validateFileName(fileName); + fileName = UUID.randomUUID().toString().replace("-", "") + "_" + fileName; + File destFile = new File(extLibDir, fileName); + FileUtils.createParentDirectories(destFile); + pluginUpload.getJarFile().transferTo(destFile); + List<PluginItem> pluginItems; + PluginMetadata pluginMetadata; + try { + PluginMetadata parsed = validateJarFile(destFile, extLibDir.getAbsolutePath()); + pluginItems = parsed.getItems(); + pluginMetadata = PluginMetadata.builder() + .name(pluginUpload.getName()) + .enableStatus(true) + .paramCount(parsed.getParamCount()) + .items(pluginItems).jarFilePath(destFile.getAbsolutePath()) + .gmtCreate(LocalDateTime.now()) + .build(); + validateMetadata(pluginMetadata); + } catch (Exception e) { + // verification failed, delete file + FileUtils.delete(destFile); + throw e; + } + // save plugin metadata + metadataDao.save(pluginMetadata); + itemDao.saveAll(pluginItems); + // load jar to classloader + loadJarToClassLoader(); + // sync enabled status + syncPluginStatus(); + } + + /** + * validate file name if file name is invalid, throw exception + * + * @param fileName file name + */ + private void validateFileName(String fileName) { + if (fileName == null) { + throw new CommonException("Failed to upload plugin"); + } + if (fileName.matches(".*(\\.\\.|[\n\t\r/\\\\]).*")) { + throw new CommonException("Invalid plugin file name: " + fileName); + } + } + + @Override + public boolean pluginIsEnable(Class<?> clazz) { + return Boolean.TRUE.equals(PLUGIN_ENABLE_STATUS.get(clazz.getName())); + } + + @Override + public Page<PluginMetadata> getPlugins(String search, int pageIndex, int pageSize) { + // Get tag information + Specification<PluginMetadata> specification = (root, query, criteriaBuilder) -> { + List<Predicate> andList = new ArrayList<>(); + if (search != null && !search.isEmpty()) { + Predicate predicateApp = criteriaBuilder.like(root.get("name"), "%" + search + "%"); + andList.add(predicateApp); + } + Predicate[] andPredicates = new Predicate[andList.size()]; + Predicate andPredicate = criteriaBuilder.and(andList.toArray(andPredicates)); + + if (andPredicates.length == 0) { + return query.where().getRestriction(); + } else { + return andPredicate; + } + }; + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize); + return metadataDao.findAll(specification, pageRequest); + } + + /** + * Load all plugin enabled states into memory + */ + @PostConstruct + private void syncPluginStatus() { + List<PluginMetadata> plugins = metadataDao.findAll(); + Map<String, Boolean> statusMap = new HashMap<>(); + Map<String, Long> itemToPluginMetadataIdMap = new HashMap<>(); + for (PluginMetadata plugin : plugins) { + for (PluginItem item : plugin.getItems()) { + statusMap.put(item.getClassIdentifier(), plugin.getEnableStatus()); + itemToPluginMetadataIdMap.put(item.getClassIdentifier(), plugin.getId()); + } + } + PLUGIN_ENABLE_STATUS.clear(); + PLUGIN_ENABLE_STATUS.putAll(statusMap); + ITEM_TO_PLUGINMETADATAID_MAP.clear(); + ITEM_TO_PLUGINMETADATAID_MAP.putAll(itemToPluginMetadataIdMap); + } + + private void syncSinglePluginStatus(PluginMetadata plugin) { + if (plugin == null || CollectionUtils.isEmpty(plugin.getItems())) { + return; + } + for (PluginItem item : plugin.getItems()) { + PLUGIN_ENABLE_STATUS.put(item.getClassIdentifier(), plugin.getEnableStatus()); + ITEM_TO_PLUGINMETADATAID_MAP.put(item.getClassIdentifier(), plugin.getId()); + } + } + + @PostConstruct + private void initParams() { + try { + List<PluginParam> params = pluginParamDao.findAll(); + Map<Long, List<PluginParam>> content = params.stream() + .collect(Collectors.groupingBy(PluginParam::getPluginMetadataId)); + + for (Map.Entry<Long, List<PluginParam>> entry : content.entrySet()) { + syncPluginParamMap(entry.getKey(), entry.getValue(), false); + } + } catch (Exception e) { + log.error("Failed to init params:{}", e.getMessage()); + throw new CommonException("Failed to init params:" + e.getMessage()); + } + } + + /** + * load jar to classloader + */ + @PostConstruct + private void loadJarToClassLoader() { + try { + for (URLClassLoader pluginClassLoader : pluginClassLoaders) { + if (pluginClassLoader != null) { + pluginClassLoader.close(); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (!pluginClassLoaders.isEmpty()) { + pluginClassLoaders.clear(); + System.gc(); + } + PARAMS_CONFIG_MAP.clear(); + List<PluginMetadata> plugins = metadataDao.findPluginMetadataByEnableStatusTrue(); + for (PluginMetadata metadata : plugins) { + try { + List<URL> urls = loadLibInPlugin(metadata.getJarFilePath(), metadata.getId()); + urls.add(new File(metadata.getJarFilePath()).toURI().toURL()); + pluginClassLoaders.add(new URLClassLoader(urls.toArray(new URL[0]), Plugin.class.getClassLoader())); + } catch (MalformedURLException e) { + log.error("Failed to load plugin:{}", e.getMessage()); + throw new CommonException("Failed to load plugin:" + e.getMessage()); + } catch (IOException exception) { + log.error("{} plugin file is missing, please delete the plugin and upload it again", metadata.getName()); + } + } + } + + /** + * loading other JAR files that are dependencies for the plugin + * + * @param pluginJarPath jar file path + * @param pluginMetadataId plugin id + * @return urls + */ + + private List<URL> loadLibInPlugin(String pluginJarPath, Long pluginMetadataId) throws IOException { + File libDir = new File(getOtherLibDir(pluginJarPath)); + FileUtils.forceMkdir(libDir); + List<URL> libUrls = new ArrayList<>(); + try (JarFile jarFile = new JarFile(pluginJarPath)) { + Enumeration<JarEntry> entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + File file = new File(libDir, entry.getName()); + String canonicalLibDir = libDir.getCanonicalPath() + File.separator; + if (!file.getCanonicalPath().startsWith(canonicalLibDir)) { + throw new IOException("Zip Slip detected: " + entry.getName()); + } + if (entry.isDirectory()) { + continue; + } + if (entry.getName().endsWith(".jar")) { + if (!file.getParentFile().exists()) { + FileUtils.createParentDirectories(file); + } + try (InputStream in = jarFile.getInputStream(entry); + OutputStream out = new FileOutputStream(file)) { + byte[] buffer = new byte[4096]; + int len; + while ((len = in.read(buffer)) != -1) { + out.write(buffer, 0, len); + } + libUrls.add(file.toURI().toURL()); + out.flush(); + } + } + if ((entry.getName().contains("define")) && (entry.getName().endsWith(".yml") || entry.getName().endsWith(".yaml"))) { + PluginConfig config = readPluginConfig(jarFile, entry); + PARAMS_CONFIG_MAP.put(pluginMetadataId, config); + } + } + } + return libUrls; + } + + /** + * Read the plugin configuration file from the jar package + * + * @return plugin config + */ + private PluginConfig readPluginConfig(JarFile jarFile, JarEntry entry) throws IOException { + Yaml yaml = new Yaml(); + try (InputStream ymlInputStream = jarFile.getInputStream(entry)) { + PluginConfig config = yaml.loadAs(ymlInputStream, PluginConfig.class); + if (config == null) { + return new PluginConfig(); + } + return config; + } + } + + @Override + public <T> void pluginExecute(Class<T> clazz, Consumer<T> execute) { + for (URLClassLoader pluginClassLoader : pluginClassLoaders) { + ServiceLoader<T> load = ServiceLoader.load(clazz, pluginClassLoader); + for (T t : load) { + if (pluginIsEnable(t.getClass())) { + execute.accept(t); + } + } + } + } + + @Override + public <T> void pluginExecute(Class<T> clazz, BiConsumer<T, PluginContext> execute) { + for (URLClassLoader pluginClassLoader : pluginClassLoaders) { + ServiceLoader<T> load = ServiceLoader.load(clazz, pluginClassLoader); + for (T t : load) { + if (!pluginIsEnable(t.getClass())) { + continue; + } + Long pluginId = ITEM_TO_PLUGINMETADATAID_MAP.get(t.getClass().getName()); + List<Configmap> configmapList = PARAMS_MAP.get(pluginId); + PluginContext context = PluginContext.builder().params(configmapList).build(); + execute.accept(t, context); + } + } + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/SmsGeneralConfigServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/SmsGeneralConfigServiceImpl.java new file mode 100644 index 0000000..e22b262 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/SmsGeneralConfigServiceImpl.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.service.impl; + +import tools.jackson.core.type.TypeReference; +import java.lang.reflect.Type; + +import jakarta.annotation.Resource; + +import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig; +import org.apache.hertzbeat.common.support.event.SmsConfigChangeEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +/** + * SmsGeneralConfigServiceImpl class is the implementation of general sms configuration service, + * which inherits the AbstractGeneralConfigServiceImpl class. + */ + +@Service +public class SmsGeneralConfigServiceImpl extends AbstractGeneralConfigServiceImpl<SmsConfig> { + @Resource + private ApplicationContext applicationContext; + + public SmsGeneralConfigServiceImpl(GeneralConfigDao generalConfigDao) { + super(generalConfigDao); + } + + /** + * This method is used to handle the sms configuration change event. + */ + @Override + public void handler(SmsConfig smsConfig) { + applicationContext.publishEvent(new SmsConfigChangeEvent(applicationContext)); + } + + @Override + public String type() { + return GeneralConfigTypeEnum.sms.name(); + } + + /** + * This method is used to get the TypeReference of NoticeSender type for subsequent processing. + * a TypeReference of NoticeSender type + */ + @Override + public TypeReference<SmsConfig> getTypeReference() { + return new TypeReference<>() { + @Override + public Type getType() { + return SmsConfig.class; + } + }; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/StatusPageServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/StatusPageServiceImpl.java new file mode 100644 index 0000000..a5d32aa --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/StatusPageServiceImpl.java @@ -0,0 +1,383 @@ +/* + * 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.manager.service.impl; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.persistence.criteria.Predicate; +import lombok.RequiredArgsConstructor; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.manager.StatusPageComponent; +import org.apache.hertzbeat.common.entity.manager.StatusPageHistory; +import org.apache.hertzbeat.common.entity.manager.StatusPageIncident; +import org.apache.hertzbeat.common.support.exception.CommonException; +import org.apache.hertzbeat.manager.component.status.CalculateStatus; +import org.apache.hertzbeat.manager.dao.StatusPageComponentDao; +import org.apache.hertzbeat.manager.dao.StatusPageHistoryDao; +import org.apache.hertzbeat.manager.dao.StatusPageIncidentComponentBindDao; +import org.apache.hertzbeat.manager.dao.StatusPageIncidentDao; +import org.apache.hertzbeat.manager.dao.StatusPageOrgDao; +import org.apache.hertzbeat.manager.pojo.dto.ComponentStatus; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageComponentInfo; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageIncidentInfo; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageOrgInfo; +import org.apache.hertzbeat.manager.service.StatusPageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +/** + * status page service implement. + */ +@Service +@RequiredArgsConstructor +public class StatusPageServiceImpl implements StatusPageService { + + private static final int HISTORY_SPAN_DAYS = 29; + + @Autowired + private StatusPageOrgDao statusPageOrgDao; + + @Autowired + private StatusPageComponentDao statusPageComponentDao; + + @Autowired + private StatusPageHistoryDao statusPageHistoryDao; + + @Autowired + private StatusPageIncidentDao statusPageIncidentDao; + + @Autowired + private CalculateStatus calculateStatus; + + private final StatusPageIncidentComponentBindDao statusPageIncidentComponentBindDao; + + + @Override + public StatusPageOrgInfo queryStatusPageOrg() { + return StatusPageOrgInfo.fromEntity(statusPageOrgDao.findAll().stream().findFirst().orElse(null)); + } + + @Override + public StatusPageOrgInfo saveStatusPageOrg(StatusPageOrgInfo statusPageOrg) { + return StatusPageOrgInfo.fromEntity(statusPageOrgDao.save(statusPageOrg.toEntity())); + } + + @Override + public List<StatusPageComponentInfo> queryStatusPageComponents() { + return statusPageComponentDao.findAll().stream().map(StatusPageComponentInfo::fromEntity).toList(); + } + + @Override + public void newStatusPageComponent(StatusPageComponentInfo statusPageComponent) { + StatusPageComponent component = statusPageComponent.toEntity(); + if (component.getMethod() == CommonConstants.STATUS_PAGE_CALCULATE_METHOD_MANUAL) { + component.setState(component.getConfigState()); + } + statusPageComponentDao.save(component); + } + + @Override + public void updateStatusPageComponent(StatusPageComponentInfo statusPageComponent) { + StatusPageComponent component = statusPageComponent.toEntity(); + if (component.getMethod() == CommonConstants.STATUS_PAGE_CALCULATE_METHOD_MANUAL) { + component.setState(component.getConfigState()); + } + statusPageComponentDao.save(component); + } + + @Override + public void deleteStatusPageComponent(long id) { + long count = statusPageIncidentComponentBindDao.countByComponentId(id); + if (count != 0) { + throw new CommonException("The component is associated with an event and cannot be deleted. Please delete the event and try again!"); + } + statusPageComponentDao.deleteById(id); + } + + @Override + public StatusPageComponentInfo queryStatusPageComponent(long id) { + return StatusPageComponentInfo.fromEntity(statusPageComponentDao.findById(id).orElse(null)); + } + + @Override + public List<ComponentStatus> queryComponentsStatus() { + List<StatusPageComponent> components = statusPageComponentDao.findAll(); + List<ComponentStatus> componentStatusList = new LinkedList<>(); + for (StatusPageComponent component : components) { + ComponentStatus componentStatus = new ComponentStatus(); + componentStatus.setInfo(component); + List<StatusPageHistory> histories = new LinkedList<>(); + // query today status + ZoneId zoneId = ZoneId.systemDefault(); + + Instant now = Instant.now(); + long nowTimestamp = now.toEpochMilli(); + + long todayStartTimestamp = now + .atZone(zoneId) + .toLocalDate() + .atStartOfDay(zoneId) + .toInstant() + .toEpochMilli(); + + List<StatusPageHistory> todayStatusPageHistoryList = statusPageHistoryDao + .findStatusPageHistoriesByComponentIdAndTimestampBetween(component.getId(), todayStartTimestamp, nowTimestamp); + StatusPageHistory todayStatus = combineOneDayStatusPageHistory(todayStatusPageHistoryList, component, nowTimestamp); + histories.add(todayStatus); + // query 30d component status history + long preTimestamp = now + .atZone(zoneId) + .toLocalDate() + .minusDays(HISTORY_SPAN_DAYS) + .atStartOfDay(zoneId) + .toInstant() + .toEpochMilli(); + + List<StatusPageHistory> history = statusPageHistoryDao + .findStatusPageHistoriesByComponentIdAndTimestampBetween(component.getId(), preTimestamp, todayStartTimestamp); + LinkedList<StatusPageHistory> historyList = new LinkedList<>(history); + historyList.sort((o1, o2) -> Long.compare(o1.getTimestamp(), o2.getTimestamp())); + ZonedDateTime end = Instant.ofEpochMilli(todayStartTimestamp) + .atZone(zoneId) + .minusSeconds(1); // yesterday 23:59:59 local time + + for (int i = 0; i < HISTORY_SPAN_DAYS; i++) { + long endTimestamp = end.toInstant().toEpochMilli(); + + long startTimestamp = end.toLocalDate() + .atStartOfDay(zoneId) + .toInstant() + .toEpochMilli(); + + List<StatusPageHistory> thisDayHistory = historyList.stream() + .filter(h -> h.getTimestamp() >= startTimestamp && h.getTimestamp() <= endTimestamp) + .collect(Collectors.toList()); + + if (thisDayHistory.isEmpty()) { + histories.add(StatusPageHistory.builder() + .timestamp(endTimestamp) + .componentId(component.getId()) + .state(CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN) + .build()); + } else if (thisDayHistory.size() == 1) { + histories.add(thisDayHistory.get(0)); + } else { + StatusPageHistory merged = + combineOneDayStatusPageHistory(thisDayHistory, component, endTimestamp); + histories.add(merged); + statusPageHistoryDao.deleteAll(thisDayHistory); + statusPageHistoryDao.save(merged); + } + + end = end.minusDays(1); + } + + componentStatus.setHistory(histories); + componentStatusList.add(componentStatus); + } + return componentStatusList; + } + + private StatusPageHistory combineOneDayStatusPageHistory(List<StatusPageHistory> statusPageHistories, StatusPageComponent component, long nowTimestamp) { + if (statusPageHistories.isEmpty()) { + return StatusPageHistory.builder().timestamp(nowTimestamp) + .normal(0).abnormal(0).unknowing(0).componentId(component.getId()).state(component.getState()).build(); + } + if (statusPageHistories.size() == 1) { + return statusPageHistories.get(0); + } + StatusPageHistory oldOne = statusPageHistories.get(0); + StatusPageHistory todayStatus = StatusPageHistory.builder().timestamp(nowTimestamp) + .normal(0).abnormal(0).unknowing(0).gmtCreate(oldOne.getGmtCreate()).gmtUpdate(oldOne.getGmtUpdate()) + .componentId(component.getId()).state(component.getState()).build(); + for (StatusPageHistory statusPageHistory : statusPageHistories) { + if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL) { + todayStatus.setAbnormal(todayStatus.getAbnormal() + calculateStatus.getCalculateStatusIntervals()); + } else if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN) { + todayStatus.setUnknowing(todayStatus.getUnknowing() + calculateStatus.getCalculateStatusIntervals()); + } else { + todayStatus.setNormal(todayStatus.getNormal() + calculateStatus.getCalculateStatusIntervals()); + } + } + double total = todayStatus.getNormal() + todayStatus.getAbnormal() + todayStatus.getUnknowing(); + double uptime = 0; + if (total > 0) { + uptime = (double) todayStatus.getNormal() / total; + } + todayStatus.setUptime(uptime); + if (todayStatus.getAbnormal() > 0) { + todayStatus.setState(CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL); + } else if (todayStatus.getNormal() > 0) { + todayStatus.setState(CommonConstants.STATUS_PAGE_COMPONENT_STATE_NORMAL); + } else { + todayStatus.setState(CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN); + } + return todayStatus; + } + + @Override + public ComponentStatus queryComponentStatus(long id) { + StatusPageComponent component = statusPageComponentDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("component not found")); + + ComponentStatus componentStatus = new ComponentStatus(); + componentStatus.setInfo(component); + List<StatusPageHistory> histories = new LinkedList<>(); + + ZoneId zoneId = ZoneId.systemDefault(); + + Instant now = Instant.now(); + long nowTimestamp = now.toEpochMilli(); + + long todayStartTimestamp = now + .atZone(zoneId) + .toLocalDate() + .atStartOfDay(zoneId) + .toInstant() + .toEpochMilli(); + + // Today + List<StatusPageHistory> todayStatusPageHistoryList = + statusPageHistoryDao.findStatusPageHistoriesByComponentIdAndTimestampBetween( + component.getId(), todayStartTimestamp, nowTimestamp); + + StatusPageHistory todayStatus = + combineOneDayStatusPageHistory(todayStatusPageHistoryList, component, nowTimestamp); + + histories.add(todayStatus); + + // Previous HISTORY_SPAN_DAYS days (excluding today) + long preTimestamp = now + .atZone(zoneId) + .toLocalDate() + .minusDays(HISTORY_SPAN_DAYS) + .atStartOfDay(zoneId) + .toInstant() + .toEpochMilli(); + + List<StatusPageHistory> history = + statusPageHistoryDao.findStatusPageHistoriesByComponentIdAndTimestampBetween( + component.getId(), preTimestamp, todayStartTimestamp); + + LinkedList<StatusPageHistory> historyList = new LinkedList<>(history); + historyList.sort((o1, o2) -> Long.compare(o1.getTimestamp(), o2.getTimestamp())); + + ZonedDateTime end = Instant.ofEpochMilli(todayStartTimestamp) + .atZone(zoneId) + .minusSeconds(1); // yesterday 23:59:59 local time + + for (int i = 0; i < HISTORY_SPAN_DAYS; i++) { + long endTimestamp = end.toInstant().toEpochMilli(); + + long startTimestamp = end.toLocalDate() + .atStartOfDay(zoneId) + .toInstant() + .toEpochMilli(); + + List<StatusPageHistory> thisDayHistory = historyList.stream() + .filter(h -> h.getTimestamp() >= startTimestamp && h.getTimestamp() <= endTimestamp) + .collect(Collectors.toList()); + + if (thisDayHistory.isEmpty()) { + histories.add(StatusPageHistory.builder() + .timestamp(endTimestamp) + .componentId(component.getId()) + .state(CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN) + .build()); + } else if (thisDayHistory.size() == 1) { + histories.add(thisDayHistory.get(0)); + } else { + StatusPageHistory merged = + combineOneDayStatusPageHistory(thisDayHistory, component, endTimestamp); + histories.add(merged); + statusPageHistoryDao.deleteAll(thisDayHistory); + statusPageHistoryDao.save(merged); + } + + end = end.minusDays(1); + } + + componentStatus.setHistory(histories); + return componentStatus; + } + + @Override + public Page<StatusPageIncidentInfo> queryStatusPageIncidents(String search, Long startTime, Long endTime, int pageIndex, int pageSize) { + // build search condition + Specification<StatusPageIncident> specification = (root, query, criteriaBuilder) -> { + List<Predicate> andList = new ArrayList<>(); + if (StringUtils.hasText(search)) { + Predicate predicateName = criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%" + search.toLowerCase() + "%"); + andList.add(predicateName); + } + if (startTime != null) { + andList.add(criteriaBuilder.greaterThanOrEqualTo(root.get("startTime"), startTime)); + } + if (endTime != null) { + andList.add(criteriaBuilder.lessThanOrEqualTo(root.get("endTime"), endTime)); + } + + Predicate[] predicates = new Predicate[andList.size()]; + return criteriaBuilder.and(andList.toArray(predicates)); + }; + + Sort sort = Sort.by(Sort.Direction.DESC, "startTime"); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sort); + return statusPageIncidentDao.findAll(specification, pageRequest).map(StatusPageIncidentInfo::fromEntity); + } + + @Override + public StatusPageIncidentInfo queryStatusPageIncident(long id) { + return StatusPageIncidentInfo.fromEntity(statusPageIncidentDao.findById(id).orElse(null)); + } + + @Override + public void newStatusPageIncident(StatusPageIncidentInfo statusPageIncident) { + StatusPageIncident incident = statusPageIncident.toEntity(); + incident.setStartTime(System.currentTimeMillis()); + if (incident.getState() == CommonConstants.STATUS_PAGE_INCIDENT_STATE_RESOLVED) { + incident.setEndTime(System.currentTimeMillis()); + } + statusPageIncidentDao.save(incident); + } + + @Override + public void updateStatusPageIncident(StatusPageIncidentInfo statusPageIncident) { + StatusPageIncident incident = statusPageIncident.toEntity(); + if (incident.getState() == CommonConstants.STATUS_PAGE_INCIDENT_STATE_RESOLVED) { + incident.setEndTime(System.currentTimeMillis()); + } + statusPageIncidentDao.save(incident); + } + + @Override + public void deleteStatusPageIncident(long id) { + statusPageIncidentDao.deleteById(id); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/SystemGeneralConfigServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/SystemGeneralConfigServiceImpl.java new file mode 100644 index 0000000..a5b39fd --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/SystemGeneralConfigServiceImpl.java @@ -0,0 +1,68 @@ +/* + * 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.manager.service.impl; + +import tools.jackson.core.type.TypeReference; +import jakarta.annotation.Resource; +import java.lang.reflect.Type; +import java.util.Objects; +import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum; +import org.apache.hertzbeat.common.support.event.SystemConfigChangeEvent; +import org.apache.hertzbeat.common.util.TimeZoneUtil; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.manager.pojo.dto.SystemConfig; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +/** + * system config service impl + */ +@Service +public class SystemGeneralConfigServiceImpl extends AbstractGeneralConfigServiceImpl<SystemConfig> { + @Resource + private ApplicationContext applicationContext; + + public SystemGeneralConfigServiceImpl(GeneralConfigDao generalConfigDao) { + super(generalConfigDao); + } + + @Override + public void handler(SystemConfig systemConfig) { + if (Objects.isNull(systemConfig)) { + return; + } + + TimeZoneUtil.setTimeZoneAndLocale(systemConfig.getTimeZoneId(), systemConfig.getLocale()); + applicationContext.publishEvent(new SystemConfigChangeEvent(applicationContext)); + } + + @Override + public String type() { + return GeneralConfigTypeEnum.system.name(); + } + + @Override + public TypeReference<SystemConfig> getTypeReference() { + return new TypeReference<>() { + @Override + public Type getType() { + return SystemConfig.class; + } + }; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/SystemSecretServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/SystemSecretServiceImpl.java new file mode 100644 index 0000000..27cfb6a --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/SystemSecretServiceImpl.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.service.impl; + +import java.lang.reflect.Type; +import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.manager.pojo.dto.SystemSecret; +import org.springframework.stereotype.Service; +import tools.jackson.core.type.TypeReference; + +/** + * system config service impl + */ +@Service +public class SystemSecretServiceImpl extends AbstractGeneralConfigServiceImpl<SystemSecret> { + + public SystemSecretServiceImpl(GeneralConfigDao generalConfigDao) { + super(generalConfigDao); + } + + @Override + public String type() { + return GeneralConfigTypeEnum.secret.name(); + } + + @Override + public TypeReference<SystemSecret> getTypeReference() { + return new TypeReference<>() { + @Override + public Type getType() { + return SystemSecret.class; + } + }; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/TemplateConfigServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/TemplateConfigServiceImpl.java new file mode 100644 index 0000000..63e4630 --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/TemplateConfigServiceImpl.java @@ -0,0 +1,63 @@ +/* + * 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.manager.service.impl; + +import tools.jackson.core.type.TypeReference; +import jakarta.annotation.Resource; +import java.lang.reflect.Type; +import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum; +import org.apache.hertzbeat.base.dao.GeneralConfigDao; +import org.apache.hertzbeat.manager.pojo.dto.TemplateConfig; +import org.apache.hertzbeat.manager.service.AppService; +import org.springframework.stereotype.Service; + +/** + * template config service impl + */ +@Service +public class TemplateConfigServiceImpl extends AbstractGeneralConfigServiceImpl<TemplateConfig> { + + @Resource + private AppService appService; + + public TemplateConfigServiceImpl(GeneralConfigDao generalConfigDao) { + super(generalConfigDao); + } + + @Override + public void handler(TemplateConfig templateConfig) { + if (templateConfig != null) { + appService.updateCustomTemplateConfig(templateConfig); + } + } + + @Override + public String type() { + return GeneralConfigTypeEnum.template.name(); + } + + @Override + protected TypeReference<TemplateConfig> getTypeReference() { + return new TypeReference<>() { + @Override + public Type getType() { + return TemplateConfig.class; + } + }; + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/YamlImExportServiceImpl.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/YamlImExportServiceImpl.java new file mode 100644 index 0000000..f43983b --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/YamlImExportServiceImpl.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.service.impl; + +import static org.apache.hertzbeat.common.constants.ExportFileConstants.YamlFile.FILE_SUFFIX; +import static org.apache.hertzbeat.common.constants.ExportFileConstants.YamlFile.TYPE; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.apache.hertzbeat.common.util.export.YamlExportUtils; +import org.yaml.snakeyaml.Yaml; + +/** + * Configure the import and export Yaml format + */ +@Slf4j +@Service +public class YamlImExportServiceImpl extends AbstractImExportServiceImpl{ + + /** + * Export file type + * @return file type + */ + @Override + public String type() { + return TYPE; + } + + /** + * Get Export File Name + * @return file name + */ + @Override + public String getFileName() { + return fileNamePrefix() + FILE_SUFFIX; + } + + /** + * Parsing an input stream into a form + * @param is input stream + * @return form + */ + @Override + public List<ExportMonitorDTO> parseImport(InputStream is) { + // todo now disable this, will enable it in the future. + // upgrade to snakeyaml 2.2 and springboot3.x to fix the issue + Yaml yaml = new Yaml(); + return yaml.load(is); + } + + /** + * Export Configuration to Output Stream + * @param monitorList configuration list + * @param os output stream + */ + @Override + public void writeOs(List<ExportMonitorDTO> monitorList, OutputStream os) { + + YamlExportUtils.exportWriteOs(monitorList, os); + } + +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/GlobalExceptionHandler.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/GlobalExceptionHandler.java new file mode 100644 index 0000000..7eed38a --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/GlobalExceptionHandler.java @@ -0,0 +1,273 @@ +/* + * 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.manager.support; + +import static org.apache.hertzbeat.common.constants.CommonConstants.DETECT_FAILED_CODE; +import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE; +import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_CONFLICT_CODE; +import static org.apache.hertzbeat.common.constants.CommonConstants.PARAM_INVALID_CODE; +import java.util.Objects; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.support.exception.CommonException; +import org.apache.hertzbeat.alert.notice.AlertNoticeException; +import org.apache.hertzbeat.manager.support.exception.MonitorDatabaseException; +import org.apache.hertzbeat.manager.support.exception.MonitorDetectException; +import org.apache.hertzbeat.manager.support.exception.MonitorMetricsException; +import org.springframework.dao.DataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +/** + * controller exception handler + */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + private static final String CONNECT_STR = "||"; + + /** + * Processing probe failure + * @param exception Detection anomaly + * @return response + */ + @ExceptionHandler(MonitorDetectException.class) + @ResponseBody + ResponseEntity<Message<Void>> handleMonitorDetectException(MonitorDetectException exception) { + Message<Void> message = Message.fail(DETECT_FAILED_CODE, exception.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(message); + } + + /** + * processing database operation exception + * @param exception Detection anomaly + * @return response + */ + @ExceptionHandler(MonitorDatabaseException.class) + @ResponseBody + ResponseEntity<Message<Void>> handleMonitorDatabaseException(MonitorDatabaseException exception) { + Message<Void> message = Message.fail(MONITOR_CONFLICT_CODE, exception.getMessage()); + return ResponseEntity.ok(message); + } + + /** + * handle common exception + * @param exception common + * @return response + */ + @ExceptionHandler(CommonException.class) + @ResponseBody + ResponseEntity<Message<Void>> handleCommonException(CommonException exception) { + Message<Void> message = Message.fail(FAIL_CODE, exception.getMessage()); + return ResponseEntity.ok(message); + } + + /** + * processing parameter error + * @param exception parameter exception + * @return response + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseBody + ResponseEntity<Message<Void>> handleIllegalArgumentException(IllegalArgumentException exception) { + Message<Void> message = Message.fail(PARAM_INVALID_CODE, exception.getMessage()); + return ResponseEntity.ok(message); + } + + @ExceptionHandler(AlertNoticeException.class) + @ResponseBody + ResponseEntity<Message<Void>> handleAlertNoticeException(AlertNoticeException noticeException) { + Message<Void> message = Message.fail(FAIL_CODE, noticeException.getMessage()); + return ResponseEntity.ok(message); + } + + /** + * processing parameter error, parameter mapping body error + * @param exception Parameter mapping body is abnormal + * @return response + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseBody + ResponseEntity<Message<Void>> handleHttpMessageNotReadableException(HttpMessageNotReadableException exception) { + try { + String msg = exception.getCause().getMessage(); + if (msg == null) { + msg = exception.getMessage(); + } + Message<Void> message = Message.fail(PARAM_INVALID_CODE, msg); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(message); + } catch (Exception e) { + Message<Void> message = Message.fail(PARAM_INVALID_CODE, exception.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(message); + } + } + + /** + * handler the exception thrown for data input verify + * @param e data input verify exception + * @return response + */ + @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class}) + @ResponseBody + ResponseEntity<Message<Void>> handleInputValidException(Exception e) { + StringBuffer errorMessage = new StringBuffer(); + if (e instanceof MethodArgumentNotValidException exception) { + exception.getBindingResult().getAllErrors().forEach(error -> { + try { + String field = Objects.requireNonNull(error.getCodes())[0]; + errorMessage.append(field).append(":").append(error.getDefaultMessage()).append(CONNECT_STR); + } catch (Exception e1) { + errorMessage.append(error.getDefaultMessage()).append(CONNECT_STR); + } + }); + } else if (e instanceof BindException exception) { + exception.getAllErrors().forEach(error -> + errorMessage.append(error.getDefaultMessage()).append(CONNECT_STR)); + } + String errorMsg = errorMessage.toString(); + if (errorMsg.endsWith(CONNECT_STR)) { + errorMsg = errorMsg.substring(0, errorMsg.length() - 2); + } + if (log.isDebugEnabled()) { + log.debug("[input argument not valid happen]-{}", errorMsg, e); + } + Message<Void> message = Message.fail(PARAM_INVALID_CODE, errorMsg); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(message); + } + + /** + * handler the exception thrown for data input verify + * @param e data input verify exception + * @return response + */ + @ExceptionHandler({ConstraintViolationException.class}) + @ResponseBody + ResponseEntity<Message<Void>> handleInputOtherValidException(ConstraintViolationException e) { + StringBuffer errorMessage = new StringBuffer(); + e.getConstraintViolations().forEach(error -> { + try { + String field = error.getPropertyPath().toString(); + errorMessage.append(field).append(":").append(error.getMessage()).append(CONNECT_STR); + } catch (Exception e1) { + errorMessage.append(error.getMessage()).append(CONNECT_STR); + } + }); + String errorMsg = errorMessage.toString(); + if (errorMsg.endsWith(CONNECT_STR)) { + errorMsg = errorMsg.substring(0, errorMsg.length() - 2); + } + log.warn("[input argument not valid happen]-{}", errorMsg); + Message<Void> message = Message.fail(PARAM_INVALID_CODE, errorMsg); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(message); + } + + /** + * handler the exception thrown for datastore error + * @param exception datastore exception + * @return response + */ + @ExceptionHandler(DataAccessException.class) + @ResponseBody + ResponseEntity<Message<Void>> handleDataAccessException(DataAccessException exception) { + String errorMessage = "database error happen"; + if (exception != null) { + errorMessage = exception.getMessage(); + } + log.warn("[database error happen]-{}", errorMessage, exception); + Message<Void> message = Message.fail(MONITOR_CONFLICT_CODE, errorMessage); + return ResponseEntity.status(HttpStatus.CONFLICT).body(message); + } + + /** + * handle Request method not supported + * @param exception Request method not supported + * @return response + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + @ResponseBody + ResponseEntity<Message<Void>> handleMethodNotSupportException(HttpRequestMethodNotSupportedException exception) { + String errorMessage = "Request method not supported"; + if (exception != null && exception.getMessage() != null) { + errorMessage = exception.getMessage(); + } + log.info("[monitor]-[Request method not supported]-{}", errorMessage); + Message<Void> message = Message.success(errorMessage); + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(message); + } + + /** + * processing monitor metrics exception + * @param exception MonitorMetricsException + * @return response + */ + @ExceptionHandler(MonitorMetricsException.class) + @ResponseBody + ResponseEntity<Message<Void>> handleMonitorMetricsException(MonitorMetricsException exception) { + Message<Void> message = Message.fail(PARAM_INVALID_CODE, exception.getMessage()); + return ResponseEntity.ok(message); + } + + /** + * handle unsupported operation exception + * @param exception UnsupportedOperationException + * @return response + */ + @ExceptionHandler(UnsupportedOperationException.class) + @ResponseBody + ResponseEntity<Message<Void>> handleUnsupportedOperationException(UnsupportedOperationException exception) { + String errorMessage = "operation not supported"; + if (exception != null && exception.getMessage() != null) { + errorMessage = exception.getMessage(); + } + log.warn("[unsupported operation]-{}", errorMessage, exception); + Message<Void> message = Message.fail(FAIL_CODE, errorMessage); + return ResponseEntity.ok(message); + } + + @ExceptionHandler(NoResourceFoundException.class) + void ignoreNoResourceFoundException(Exception ex) throws Exception { + throw ex; + } + + /** + * handler the exception thrown for unCatch and unKnown + * @param exception UnknownException + * @return response + */ + @ExceptionHandler(Exception.class) + @ResponseBody + ResponseEntity<Message<Void>> handleUnknownException(Exception exception) { + String errorMessage = "unknown error happen"; + if (exception != null) { + errorMessage = exception.getMessage(); + } + log.error("[monitor]-[unknown error happen]-{}", errorMessage, exception); + Message<Void> message = Message.fail(MONITOR_CONFLICT_CODE, errorMessage); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(message); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/exception/MonitorDatabaseException.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/exception/MonitorDatabaseException.java new file mode 100644 index 0000000..276fa8f --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/exception/MonitorDatabaseException.java @@ -0,0 +1,27 @@ +/* + * 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.manager.support.exception; + +/** + * Database operation exception + */ +public class MonitorDatabaseException extends RuntimeException { + public MonitorDatabaseException(String message) { + super(message); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/exception/MonitorDetectException.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/exception/MonitorDetectException.java new file mode 100644 index 0000000..84a614f --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/exception/MonitorDetectException.java @@ -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.manager.support.exception; + +/** + * Detection failure exception + */ +public class MonitorDetectException extends RuntimeException { + + public MonitorDetectException(String message) { + super(message); + } +} diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/exception/MonitorMetricsException.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/exception/MonitorMetricsException.java new file mode 100644 index 0000000..d64064b --- /dev/null +++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/exception/MonitorMetricsException.java @@ -0,0 +1,27 @@ +/* + * 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.manager.support.exception; + +/** + * Description: Abnormal monitoring indicators + */ +public class MonitorMetricsException extends RuntimeException { + public MonitorMetricsException(String message) { + super(message); + } +} diff --git a/hertzbeat-manager/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hertzbeat-manager/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..35ed8bf --- /dev/null +++ b/hertzbeat-manager/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,16 @@ +# 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. + +org.apache.hertzbeat.manager.config.ManagerAutoConfiguration diff --git a/hertzbeat-manager/src/main/resources/define/app-a_example.yml b/hertzbeat-manager/src/main/resources/define/app-a_example.yml new file mode 100644 index 0000000..04a8b83 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-a_example.yml @@ -0,0 +1,257 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: custom +# The monitoring type eg: linux windows tomcat mysql aws... +app: a_example +# The monitoring i18n name +name: + zh-CN: 模拟应用 + en-US: EXAMPLE APP +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 支持自定义监控,您只需配置监控模版 YML 就能适配一款自定义的监控类型。<br>定义流程如下:HertzBeat 页面 -> 监控模版菜单 -> 新增监控类型 -> 配置自定义监控模版YML -> 点击保存应用 -> 使用新监控类型添加监控。 + en-US: "HertzBeat supports custom monitoring, and you only need to configure the monitoring template YML to adapt to a custom monitoring type. <br>Definition process as follow: HertzBeat Pages -> Main Menu -> Monitor Template -> edit and save -> apply this template." + zh-TW: HertzBeat支持自定義監控,您只需配寘監控模版YML就能適配一款自定義的監控類型。<br>定義流程如下:HertzBeat頁面->監控模版選單->新增監控類型->配寘自定義監控模版YML ->點擊保存應用->使用新監控類型添加監控。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-point/ + en-US: https://hertzbeat.apache.org/docs/advanced/extend-point/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 80 + # param field input placeholder + placeholder: 'Please Input Port' + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 用户密码 + en-US: Password + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + # type-param field type(boolean mapping the html switch tag) + type: boolean + # required-true or false + required: false + # field-param field key + - field: method + # name-param field display i18n name + name: + zh-CN: 请求方式 + en-US: Method + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: GET + value: GET + - label: POST + value: POST + - label: PUT + value: PUT + - label: DELETE + value: DELETE + # field-param field key + - field: headers + # name-param field display i18n name + name: + zh-CN: 请求Headers + en-US: Headers + # type-param field type(key-value mapping the html key-value input tags) + type: key-value + # required-true or false + required: false + # when type is key-value, use keyAlias to config key alias name + keyAlias: Header Name + # when type is key-value, use valueAlias to config value alias name + valueAlias: Header Value +# collect metrics config list +metrics: + # metrics - cpu + - name: cpu + # metrics name i18n label + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, i18n-metric name i18n label, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + - field: cores + type: 0 + i18n: + zh-CN: 核数 + en-US: Cores + - field: waitTime + type: 0 + unit: s + i18n: + zh-CN: 主机名称 + en-US: Host Name + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - hostname + - core1 + - core2 + - usage + - allTime + - runningTime + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - hostname=hostname + - cores=core1+core2 + - usage=usage + - waitTime=allTime-runningTime + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /metrics/cpu + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: false + # http request header content + headers: + ^_^headers^_^: ^_^headers^_^ + # http request params + params: + param1: param1 + param2: param2 + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$' + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + priority: 1 + fields: + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Hostname + - field: total + type: 0 + unit: kb + i18n: + zh-CN: 总量 + en-US: Total + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + - field: speed + type: 0 + i18n: + zh-CN: 速率 + en-US: Speed + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /metrics/memory + method: GET + headers: + apiVersion: v1 + params: + param1: param1 + param2: param2 + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: default diff --git a/hertzbeat-manager/src/main/resources/define/app-activemq.yml b/hertzbeat-manager/src/main/resources/define/app-activemq.yml new file mode 100644 index 0000000..7d4f5f5 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-activemq.yml @@ -0,0 +1,593 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: mid +# The monitoring type eg: linux windows tomcat mysql aws... +app: activemq +# The monitoring i18n name +name: + zh-CN: ActiveMQ消息系统 + en-US: ActiveMQ Message + ja-JP: ActiveMQメッセージングシステム +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMX 协议</a> 对 Apache ActiveMQ 消息中间件的运行状态,节点,Topic等相关指标(broker、topic、memory pool、class loading、thread)进行监测。<br><span class='help_module_span'>⚠️注意:您需要在 ActiveMQ 开启 JMX 服务。<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/activemq'>点击查看开启步骤</a>。</span> + en-US: "HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMX protocol</a> to monitor the running status, nodes, topics, and other metrics of Apache ActiveMQ message-oriented middleware. <br><span class='help_module_span'>⚠️Note: You should enable the JMX service in ActiveMQ. <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/activemq'>Click here to view the specific steps.</a></span>" + zh-TW: HertzBeat使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMX 協定</a> 對 Apache ActiveMQ 消息中介軟體的運行狀態,節點,Topic 等相關名額(broker、topic、memory pool、class loading、thread)進行監測。<br><span class='help_module_span'> ⚠️注意:您需要在 ActiveMQ 開啟 JMX 服務。<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/activemq'>點擊查看開啟步驟</a>。</span> + ja-JP: HertzBeatは <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMXプロトコルを介して</a> Apache ActiveMQメッセージングシステムのランタイムステータス、ノード、トピック、その他の関連メトリクスを監視します。<br><span class='help_module_span'> ⚠️注意:ActiveMQ で JMX サービスを有効にする必要があります。<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/activemq'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/activemq + en-US: https://hertzbeat.apache.org/docs/help/activemq +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标 Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: JMX 端口 + en-US: JMX Port + ja-JP: JMXポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 11099 + # field-param field key + - field: url + # name-param field display i18n name + name: + zh-CN: JMX URL + en-US: JMX URL + ja-JP: JMX URL + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + # param field input placeholder + placeholder: 'service:jmx:rmi:///jndi/rmi://host:port/jmxrmi' + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 虚拟机基础信息 + en-US: JVM Basic + ja-JP: Java仮想マシン基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: VmName + type: 1 + i18n: + zh-CN: 名称 + en-US: Vm Name + ja-JP: 仮想マシン名 + - field: VmVendor + type: 1 + i18n: + zh-CN: 厂商 + en-US: Vm Vendor + ja-JP: 仮想マシンベンダー + - field: VmVersion + type: 1 + i18n: + zh-CN: 版本 + en-US: Vm Version + ja-JP: 仮想マシンバージョン + - field: Uptime + type: 0 + unit: ms + i18n: + zh-CN: 运行时长 + en-US: Up time + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jmx + # the config content when protocol is jmx + jmx: + # jmx host: ipv4 ipv6 domain + host: ^_^host^_^ + # jmx port + port: ^_^port^_^ + # jmx username + username: ^_^username^_^ + # jmx password + password: ^_^password^_^ + # jmx mbean object name + objectName: java.lang:type=Runtime + # jmx url + url: ^_^url^_^ + + - name: broker + i18n: + zh-CN: Broker 信息 + en-US: Broker Info + ja-JP: ブローカー情報 + priority: 1 + fields: + - field: BrokerName + type: 1 + label: true + i18n: + zh-CN: Broker 名称 + en-US: Broker Name + ja-JP: ブローカー名 + - field: BrokerVersion + i18n: + zh-CN: Broker 版本 + en-US: Broker Version + ja-JP: ブローカーバージョン + type: 1 + - field: Uptime + i18n: + zh-CN: 启动时长 + en-US: Uptime + ja-JP: アップタイム + type: 1 + - field: UptimeMillis + i18n: + zh-CN: 启动时长(毫秒) + en-US: Uptime Millis + ja-JP: アップタイム(ms) + type: 0 + unit: ms + - field: Persistent + i18n: + zh-CN: 持久化 + en-US: Persistence + ja-JP: 永続化 + type: 1 + - field: DataDirectory + i18n: + zh-CN: 数据目录 + en-US: Data Directory + ja-JP: データディレクトリ + type: 1 + - field: MemoryPercentUsage + i18n: + zh-CN: 内存使用率 + en-US: Memory Percent Usage + ja-JP: メモリ使用率 + type: 0 + unit: '%' + - field: StorePercentUsage + i18n: + zh-CN: 存储使用率 + en-US: Store Percent Usage + ja-JP: ストレージ使用率 + type: 0 + unit: '%' + - field: TempPercentUsage + i18n: + zh-CN: 临时使用率 + en-US: Temp Percent Usage + ja-JP: 一時使用率 + type: 0 + unit: '%' + - field: CurrentConnectionsCount + i18n: + zh-CN: 当前连接数 + en-US: Current Connections Count + ja-JP: 現在接続数 + type: 0 + - field: TotalConnectionsCount + i18n: + zh-CN: 总连接数 + en-US: Total Connections Count + ja-JP: 合計接続数 + type: 0 + - field: TotalEnqueueCount + i18n: + zh-CN: 总入队数 + en-US: Total Enqueue Count + ja-JP: 合計エンキュー数 + type: 0 + - field: TotalDequeueCount + i18n: + zh-CN: 总出队数 + en-US: Total Dequeue Count + ja-JP: 合計デキュー数 + type: 0 + - field: TotalConsumerCount + i18n: + zh-CN: 消费者总数 + en-US: Total Consumer Count + ja-JP: 消費者総数 + type: 0 + - field: TotalProducerCount + i18n: + zh-CN: 生产者总数 + en-US: Total Producer Count + ja-JP: 生産者総数 + type: 0 + - field: TotalMessageCount + i18n: + zh-CN: 总消息数 + en-US: Total Message Count + ja-JP: メッセージ総数 + type: 0 + - field: AverageMessageSize + i18n: + zh-CN: 平均消息大小 + en-US: Average Message Size + ja-JP: メッセージの平均サイズ + type: 0 + - field: MaxMessageSize + i18n: + zh-CN: 最大消息大小 + en-US: Max Message Size + ja-JP: メッセージの最大サイズ + type: 0 + - field: MinMessageSize + i18n: + zh-CN: 最小消息大小 + en-US: Min Message Size + ja-JP: メッセージの最小サイズ + type: 0 + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: org.apache.activemq:type=Broker,brokerName=* + url: ^_^url^_^ + + - name: topic + priority: 1 + fields: + - field: Name + type: 1 + label: true + i18n: + zh-CN: Topic 名称 + en-US: Topic Name + ja-JP: トピック名 + - field: MemoryLimit + type: 0 + unit: MB + i18n: + zh-CN: 内存限制 + en-US: Memory Limit + ja-JP: メモリ制限 + - field: MemoryPercentUsage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Percent Usage + ja-JP: メモリ使用率 + - field: ProducerCount + type: 0 + i18n: + zh-CN: 生产者数量 + en-US: Producer Count + ja-JP: 生産者数 + - field: ConsumerCount + type: 0 + i18n: + zh-CN: 消费者数量 + en-US: Consumer Count + ja-JP: 消費者数 + - field: EnqueueCount + type: 0 + i18n: + zh-CN: 入队数量 + en-US: Enqueue Count + ja-JP: エンキュー数 + - field: DequeueCount + type: 0 + i18n: + zh-CN: 出队数量 + en-US: Dequeue Count + ja-JP: デキュー数 + - field: ForwardCount + type: 0 + i18n: + zh-CN: 转发数量 + en-US: Forward Count + ja-JP: フォワード数 + - field: InFlightCount + type: 0 + i18n: + zh-CN: 进行中数量 + en-US: In Flight Count + ja-JP: 処理数 + - field: DispatchCount + type: 0 + i18n: + zh-CN: 分发数量 + en-US: Dispatch Count + ja-JP: ディスパッチ数 + - field: ExpiredCount + type: 0 + i18n: + zh-CN: 过期数量 + en-US: Expired Count + ja-JP: 期限切れ数 + - field: StoreMessageSize + type: 0 + unit: B + i18n: + zh-CN: 存储消息大小 + en-US: Store Message Size + ja-JP: ストレージメッセージのサイズ + - field: AverageEnqueueTime + type: 0 + unit: ms + i18n: + zh-CN: 平均入队时间 + en-US: Average Enqueue Time + ja-JP: 平均エンキュー時間 + - field: MaxEnqueueTime + type: 0 + unit: ms + i18n: + zh-CN: 最大入队时间 + en-US: Max Enqueue Time + ja-JP: 最大エンキュー時間 + - field: MinEnqueueTime + type: 0 + unit: ms + i18n: + zh-CN: 最小入队时间 + en-US: Min Enqueue Time + ja-JP: 最小エンキュー時間 + - field: TotalBlockedTime + type: 0 + unit: ms + i18n: + zh-CN: 总阻塞时间 + en-US: Total Blocked Time + ja-JP: 合計ブロック時間 + - field: AverageMessageSize + type: 0 + unit: B + i18n: + zh-CN: 平均消息大小 + en-US: Average Message Size + ja-JP: メッセージの平均サイズ + - field: MaxMessageSize + type: 0 + unit: B + i18n: + zh-CN: 最大消息大小 + en-US: Max Message Size + ja-JP: メッセージの最大サイズ + - field: MinMessageSize + type: 0 + unit: B + i18n: + zh-CN: 最小消息大小 + en-US: Min Message Size + ja-JP: メッセージの最小サイズ + units: + - MemoryLimit=B->MB + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: org.apache.activemq:type=Broker,brokerName=*,destinationType=Topic,destinationName=* + url: ^_^url^_^ + + - name: memory_pool + i18n: + zh-CN: 内存池 + en-US: Memory Pool + ja-JP: メモリプール + priority: 2 + fields: + - field: name + type: 1 + label: true + i18n: + zh-CN: 指标名称 + en-US: Name + ja-JP: メトリクス名 + - field: committed + type: 0 + unit: MB + i18n: + zh-CN: 已分配内存 + en-US: Committed + ja-JP: コミットされたメモリ + - field: init + type: 0 + unit: MB + i18n: + zh-CN: 初始化内存 + en-US: Init + ja-JP: イニシャルメモリ + - field: max + type: 0 + unit: MB + i18n: + zh-CN: 最大内存 + en-US: Max + ja-JP: 最大メモリ + - field: used + type: 0 + unit: MB + i18n: + zh-CN: 已使用内存 + en-US: Used + ja-JP: 使用したメモリ + units: + - committed=B->MB + - init=B->MB + - max=B->MB + - used=B->MB + aliasFields: + - Name + - Usage->committed + - Usage->init + - Usage->max + - Usage->used + calculates: + - name=Name + - committed=Usage->committed + - init=Usage->init + - max=Usage->max + - used=Usage->used + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=MemoryPool,name=* + url: ^_^url^_^ + + - name: class_loading + i18n: + zh-CN: 类加载信息 + en-US: Class Loading + ja-JP: クラスローディング情報 + priority: 2 + fields: + - field: LoadedClassCount + type: 0 + i18n: + zh-CN: 当前已加载类数量 + en-US: Loaded Class Count + ja-JP: ロードされたクラス数 + - field: TotalLoadedClassCount + type: 0 + i18n: + zh-CN: 已加载类总数量 + en-US: Total Loaded Class Count + ja-JP: ロードされたクラス総数 + - field: UnloadedClassCount + type: 0 + i18n: + zh-CN: 未加载类总数量 + en-US: Unloaded Class Count + ja-JP: アンロードされたクラス総数 + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=ClassLoading + url: ^_^url^_^ + + - name: thread + i18n: + zh-CN: 线程信息 + en-US: Thread + ja-JP: スレッド情報 + priority: 2 + fields: + - field: TotalStartedThreadCount + type: 0 + i18n: + zh-CN: 已启动线程总数 + en-US: Total Started Thread Count + ja-JP: スレッド総数 + - field: ThreadCount + type: 0 + i18n: + zh-CN: 活跃线程数 + en-US: Thread Count + ja-JP: 活躍スレッド数 + - field: PeakThreadCount + type: 0 + i18n: + zh-CN: 最大峰值线程数 + en-US: Peak Thread Count + ja-JP: 最大スレッド数 + - field: DaemonThreadCount + type: 0 + i18n: + zh-CN: 活跃守护线程数 + en-US: Daemon Thread Count + ja-JP: デーモンスレッド数 + - field: CurrentThreadUserTime + type: 0 + unit: s + i18n: + zh-CN: 线程占用的CPU时间(用户态) + en-US: Current Thread User Time + ja-JP: 現在のスレッドユーザー時間 + - field: CurrentThreadCpuTime + type: 0 + unit: s + i18n: + zh-CN: 线程占用的CPU时间 + en-US: Current Thread CPU Time + ja-JP: 現在のスレッドシステム時間 + units: + - CurrentThreadUserTime=NS->S + - CurrentThreadCpuTime=NS->S + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=Threading + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-airflow.yml b/hertzbeat-manager/src/main/resources/define/app-airflow.yml new file mode 100644 index 0000000..475f47e --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-airflow.yml @@ -0,0 +1,158 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring mid-middleware +category: bigdata +# The monitoring: airflow +app: airflow +# The monitoring i18n name +name: + zh-CN: Apache Airflow + en-US: Apache Airflow + ja-JP: Apache Airflow +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Apache Airflow 通用性能指标(airflow health、airflow version)进行采集监控。<br>您可以点击“<i>新建 Apache Airflow </i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat monitors Apache Airflow through general performance metrics such as airflow health and airflow version. You could click the "<i>New Apache Airflow</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat對Apache Airflow通用性能指標(airflow health、airflow version)進行採集監控。<br>您可以點擊“<i>新建 Apache Airflow</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeatは、Apache Airflowの一般的なパフォーマンスのメトリクスを監視します。 <br><i>新規Apache Airflow</i>をクリックして設定しましょう。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/airflow/ + en-US: https://hertzbeat.apache.org/docs/help/airflow/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 服务器 Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # required-true or false + - field: port + # name-param field display i18n name + name: + zh-CN: 服务端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # required-true or false + defaultValue: 8080 + + - field: ssl + name: + zh-CN: 启用HTTPS + en-US: HTTPS + ja-JP: HTTPS + type: boolean + hide: true + required: true + + - field: timeout + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + type: number + hide: true + required: false + +# collect metrics config list +metrics: + - name: airflow_health + i18n: + zh-CN: airflow 健康状态 + en-US: airflow health status + ja-JP: airflow 健全状態 + priority: 0 + fields: + - field: metadatabase + i18n: + zh-CN: metadatabase 健康情况 + en-US: metadatabase status + ja-JP: metadatabase 健全状態 + type: 1 + - field: scheduler + i18n: + zh-CN: scheduler 健康情况 + en-US: scheduler status + ja-JP: scheduler 健全状態 + type: 1 + - field: triggerer + i18n: + zh-CN: triggerer 健康情况 + en-US: triggerer status + ja-JP: triggerer 健全状態 + type: 1 + aliasFields: + - $.metadatabase.status + - $.scheduler.status + - $.triggerer.status + calculates: + - metadatabase=$.metadatabase.status + - scheduler=$.scheduler.status + - triggerer=$.triggerer.status + # monitoring and collection usage protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # This section describes how to configure data collection when protocol is http + http: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url Indicates the path of the request interface + url: /api/v1/health + timeout: ^_^timeout^_^ + method: GET + parseType: jsonPath + parseScript: '$' + + - name: airflow_version + i18n: + zh-CN: Airflow 版本 + en-US: Airflow version + ja-JP: Airflowバージョン + priority: 1 + fields: + - field: version + i18n: + zh-CN: Airflow 版本 + en-US: Airflow version + ja-JP: Airflowバージョン + type: 1 + - field: git_version + i18n: + zh-CN: git版本 + en-US: git version + ja-JP: Gitバージョン + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /api/v1/version + timeout: ^_^timeout^_^ + method: GET + parseType: jsonPath + parseScript: '$' diff --git a/hertzbeat-manager/src/main/resources/define/app-almalinux.yml b/hertzbeat-manager/src/main/resources/define/app-almalinux.yml new file mode 100644 index 0000000..5a24a8a --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-almalinux.yml @@ -0,0 +1,765 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: almalinux +# The monitoring i18n name +name: + zh-CN: AlmaLinux + en-US: AlmaLinux + ja-JP: AlmaLinux +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 AlmaLinux 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 AlmaLinux</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors AlmaLinux operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New AlmaLinux</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 AlmaLinux 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建AlmaLinux</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> AlmaLinuxシステムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 AlmaLinux</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/almalinux + en-US: https://hertzbeat.apache.org/docs/help/almalinux +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标 Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: Private Key + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: (uname -r ; hostname ; uptime | awk -F "," '{print $1}' | sed "s/ //g") | sed ":a;N;s/\n/^/g;ta" | awk -F '^' 'BEGIN{print "version hostname uptime"} {print $1, $2, $3}' + # ssh response data parse type: oneRow, multiRow + parseType: multiRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100-idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: "LANG=C lscpu | awk -F: '$1==\"Model name\" {print $2}';awk '/processor/{core++} END{print core}' /proc/cpuinfo;uptime | sed 's/,/ /g' | awk '{for(i=NF-2;i<=NF;i++)print $i }' | xargs;vmstat 1 1 | awk 'NR==3{print $11}';vmstat 1 1 | awk 'NR==3{print $12}';vmstat 1 2 | awk 'NR==4{print $15}'" + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: free -m | awk 'BEGIN{print "total used free buff_cache available"} NR==2{print $2,$3,$4,$6,$7}' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + en-US: Partition Num + ja-JP: パーティション + - field: block_write + type: 0 + i18n: + zh-CN: 写磁盘块数 + en-US: Block Write + ja-JP: 書き込みディスクブロック数 + - field: block_read + type: 0 + i18n: + zh-CN: 读磁盘块数 + en-US: Block Read + ja-JP: 読み取りブロック数 + - field: write_rate + type: 0 + unit: iops + i18n: + zh-CN: 磁盘写速率 + en-US: Write Rate + ja-JP: ディスク書き込み速度 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: vmstat -D | awk 'NR==1{print $1}';vmstat -D | awk 'NR==2{print $1}';vmstat 1 1 | awk 'NR==3{print $10}';vmstat 1 1 | awk 'NR==3{print $9}';vmstat 1 1 | awk 'NR==3{print $16}' + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: cat /proc/net/dev | tail -n +3 | awk 'BEGIN{ print "interface_name receive_bytes transmit_bytes"} {print $1,$2,$10}' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -mP | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU 占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU 占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-api.yml b/hertzbeat-manager/src/main/resources/define/app-api.yml new file mode 100644 index 0000000..2d5386c --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-api.yml @@ -0,0 +1,403 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: api +# The app api i18n name +name: + zh-CN: HTTP API + en-US: HTTP API + ja-JP: HTTP API +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 将调用 HTTP API 接口,查看接口是否可用,并以 ms 为指标单位对其响应时间等指标进行监测。您可以先点击“新增 HTTP API”按钮并进行配置,或在“更多操作”中导入已有配置。 + en-US: The platform will invoke an HTTP API endpoint to verify its accessibility and monitor various metrics, including response time, measured in milliseconds (ms). <br>To set up this functionality, you could click the "Add HTTP API" button and proceed with the configuration or import an existing setup through the "More Actions" menu. + zh-TW: Hertzbeat將調用HTTP API介面,查看介面是否可用,並以ms為名額組織對其回應時間等名額進行監測。 您可以先點擊“新增HTTP API”按鈕並進行配寘,或在“更多操作”中導入已有配寘。 + ja-JP: Hertzbeatは、HTTP APIを呼び出して利用可能かどうかを確認し、応答時間(ms)などのメトリクスを監視します。「新規HTTP API」をクリックして設定しましょう。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/api + en-US: https://hertzbeat.apache.org/docs/help/api +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 80 + # field-param field key + - field: httpMethod + # name-param field display i18n name + name: + zh-CN: 请求方式 + en-US: Method + ja-JP: リクエストメソッド + # type-param field type(most mapping the html input type) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: GET + value: GET + - label: POST + value: POST + - label: PUT + value: PUT + - label: DELETE + value: DELETE + # field-param field key + - field: uri + # name-param field display i18n name + name: + zh-CN: 相对路径 + en-US: URI + ja-JP: URI + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 200 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Website uri path(no ip port) EG:/v2/book/bar' + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启用HTTPS + en-US: HTTPS + ja-JP: HTTPS利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: headers + # name-param field display i18n name + name: + zh-CN: 请求Headers + en-US: Headers + ja-JP: ヘッダ + # type-param field type(most mapping the html input type) + type: key-value + # required-true or false + required: false + # when type is key-value, use keyAlias to config key alias name + keyAlias: Header Name + # when type is key-value, use valueAlias to config value alias name + valueAlias: Header Value + # field-param field key + - field: params + # name-param field display i18n name + name: + zh-CN: 查询Params + en-US: Params + ja-JP: パラメータ + # type-param field type(most mapping the html input type) + type: key-value + # required-true or false + required: false + # when type is key-value, use keyAlias to config key alias name + keyAlias: Param Key + # when type is key-value, use valueAlias to config value alias name + valueAlias: Param Value + # field-param field key + - field: contentType + # name-param field display i18n name + name: + zh-CN: Content-Type + en-US: Content-Type + ja-JP: コンテンツタイプ + # type-param field type(most mapping the html input type) + type: text + # param field input placeholder + placeholder: 'Request Body Type' + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: payload + # name-param field display i18n name + name: + zh-CN: 请求BODY + en-US: BODY + ja-JP: ボディ + # type-param field type(most mapping the html input type) + type: textarea + # param field input placeholder + placeholder: 'Available When POST PUT' + # dependent parameter values list + depend: + httpMethod: + - POST + - PUT + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: authType + # name-param field display i18n name + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + # type-param field type(most mapping the html input type) + type: radio + # required-true or false + required: false + # hide param-true or false + hide: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: keyword + # name-param field display i18n name + name: + zh-CN: 计数关键字 + en-US: Keyword + ja-JP: キーワード + # type-param field type(most mapping the html input type) + type: text + # param field input placeholder + placeholder: 'Enter keyword which occurrences need to be monitored' + # required-true or false + required: false + # hide param-true or false + hide: true + # filed-param field key + - field: successCode + # name-param field display i18n name + name: + zh-CN: 校验成功状态码 + en-US: Verify Success Code + ja-JP: 成功ステータスコードの検証 + # type-param field type(most mapping the html input type) + type: array + # param field input placeholder + placeholder: 'Input Http Status Code' + # required-true or false + required: false + defaultValue: '200, 201' + # hide param-true or false + hide: true + # field-param field key + - field: enableUrlEncoding + # name-param field display i18n name + name: + zh-CN: 启用URL编码(UTF-8) + en-US: Enable URL Encoding(UTF-8) + ja-JP: URL Encoding利用(UTF-8) + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + # default value + defaultValue: true + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - summary + - name: summary + i18n: + zh-CN: 概要 + en-US: Summary + ja-JP: 概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: keyword + type: 0 + i18n: + zh-CN: 关键词数量 + en-US: Keyword + ja-JP: キーワード + - field: statusCode + type: 0 + i18n: + zh-CN: 状态码 + en-US: Status Code + ja-JP: 状態コード + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^uri^_^ + # http connect timeout + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH + method: ^_^httpMethod^_^ + # if enabled https + ssl: ^_^ssl^_^ + # enable url encoding + enableUrlEncoding: ^_^enableUrlEncoding^_^ + # http request payload + payload: ^_^payload^_^ + # http request header content + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + # http request params + params: + ^_^params^_^: ^_^params^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + #http success status code. default 200 + #successCode means what http response status code we consider it collect success. + successCodes: [^_^successCode^_^] + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: website + keyword: ^_^keyword^_^ + - name: header + i18n: + zh-CN: 请求头 + en-US: Header + ja-JP: ヘッダ + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: Content-Type + type: 1 + i18n: + zh-CN: Content Type + en-US: Content Type + ja-JP: コンテンツタイプ + - field: Content-Length + type: 1 + i18n: + zh-CN: 响应内容长度 + en-US: Content Length + ja-JP: コンテンツの長さ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^uri^_^ + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + # enable url encoding + enableUrlEncoding: ^_^enableUrlEncoding^_^ + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: header + keyword: ^_^keyword^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-api_code.yml b/hertzbeat-manager/src/main/resources/define/app-api_code.yml new file mode 100644 index 0000000..a298358 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-api_code.yml @@ -0,0 +1,336 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: api_code +# The app api i18n name +name: + zh-CN: API业务状态码 + en-US: API Code + ja-JP: APIコード +# The description and help of this monitoring type +help: + zh-CN: 监控 HTTP API 接口,对 API 返回的业务自定义状态码(非 Http 状态码)进行监控。此需通过配置 JsonPath 来解析您 API 的业务状态码路径。<br>您可以点击 “<i>新建 API Code</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Monitor HTTP API to monitor business-defined status codes (non-HTTP status codes) returned by API. To do this, you need to configure JsonPath to resolve the business status code path of your API. <br>You could click the "<i>New API Codes</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: 監控 HTTP API 接口,對 API 返回的業務自定義狀態碼(非 Http 狀態碼)進行監控。此需通過配置 JsonPath 來解析您 API 的業務狀態碼路徑。<br>您可以點擊“<i>新建API Code</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeatは、HTTP APIを呼び出して利用可能かどうかを確認し、ビジネスステータスコード(HTTPステータスコードではない)のメトリクスを監視します。これには、API のビジネスステータスコードのパスを解析するように JsonPath を構成する必要があります。「新規API Code」をクリックして設定しましょう。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/api_code + en-US: https://hertzbeat.apache.org/docs/help/api_code +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 80 + # field-param field key + - field: httpMethod + # name-param field display i18n name + name: + zh-CN: 请求方式 + en-US: Method + ja-JP: リクエストメソッド + # type-param field type(most mapping the html input type) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: GET + value: GET + - label: POST + value: POST + - label: PUT + value: PUT + - label: DELETE + value: DELETE + # field-param field key + - field: uri + # name-param field display i18n name + name: + zh-CN: 相对路径 + en-US: URI + ja-JP: URI + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 200 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Website uri path(no ip port) EG:/v2/book/bar' + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启用HTTPS + en-US: HTTPS + ja-JP: HTTPS利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: jsonPath + # name-param field display i18n name + name: + zh-CN: JsonPath解析状态码路径 + en-US: JsonPath Parse Code + ja-JP: JsonPath 解析ステータスコード + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true + # default value + defaultValue: $.code + # param field input placeholder + placeholder: 'The jsonPath to parse code field in response data' + # field-param field key + - field: headers + # name-param field display i18n name + name: + zh-CN: 请求Headers + en-US: Headers + ja-JP: ヘッダ + # type-param field type(most mapping the html input type) + type: key-value + # required-true or false + required: false + # when type is key-value, use keyAlias to config key alias name + keyAlias: Header Name + # when type is key-value, use valueAlias to config value alias name + valueAlias: Header Value + # hide param-true or false + hide: true + - field: params + # name-param field display i18n name + name: + zh-CN: 查询Params + en-US: Params + ja-JP: パラメータ + # type-param field type(most mapping the html input type) + type: key-value + # required-true or false + required: false + # when type is key-value, use keyAlias to config key alias name + keyAlias: Param Key + # when type is key-value, use valueAlias to config value alias name + valueAlias: Param Value + # hide param-true or false + hide: true + # field-param field key + - field: contentType + # name-param field display i18n name + name: + zh-CN: Content-Type + en-US: Content-Type + ja-JP: コンテンツタイプ + # type-param field type(most mapping the html input type) + type: text + # param field input placeholder + placeholder: 'Request Body Type' + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: payload + # name-param field display i18n name + name: + zh-CN: 请求BODY + en-US: BODY + ja-JP: ボディ + # type-param field type(most mapping the html input type) + type: textarea + # param field input placeholder + placeholder: 'Available When POST PUT' + # dependent parameter values list + depend: + httpMethod: + - POST + - PUT + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: authType + # name-param field display i18n name + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + # type-param field type(most mapping the html input type) + type: radio + # required-true or false + required: false + # hide param-true or false + hide: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + - label: Bearer Token + value: Bearer Token + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: token + # name-param field display i18n name + name: + zh-CN: 认证Token + en-US: Access Token + ja-JP: アクセストークン + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: keyword + # name-param field display i18n name + name: + zh-CN: 计数关键字 + en-US: Keyword + ja-JP: キーワード + # type-param field type(most mapping the html input type) + type: text + # param field input placeholder + placeholder: 'Enter keyword which occurrences need to be monitored' + # required-true or false + required: false + # hide param-true or false + hide: true + +metrics: + # metrics - status + - name: status + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: code + type: 1 + i18n: + zh-CN: 业务状态码 + en-US: Status Code + ja-JP: ステータスコード + aliasFields: + - ^_^jsonPath^_^ + - responseTime + calculates: + - code=^_^jsonPath^_^ + - responseTime=responseTime + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + timeout: ^_^timeout^_^ + method: ^_^httpMethod^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + ^_^params^_^: ^_^params^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + bearerTokenToken: ^_^token^_^ + parseType: jsonPath + parseScript: '$' diff --git a/hertzbeat-manager/src/main/resources/define/app-apollo.yml b/hertzbeat-manager/src/main/resources/define/app-apollo.yml new file mode 100644 index 0000000..9194032 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-apollo.yml @@ -0,0 +1,438 @@ +# 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. + +# The monitoring type category: service-application service monitoring, db-database monitoring, custom-custom monitoring. +category: mid +# The monitoring application type name (consistent with the file name) +app: apollo +# The monitoring i18n name +name: + zh-CN: Apollo配置中心 + en-US: Apollo Configuration Center +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 Apollo 配置中心的通用指标进行测量监控。<br>您可以点击 “<i>新建 Apollo 配置中心</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。<br>⚠️注意:adminservice、configservice、portal目前需要分别配置,只需要更改地址+端口即可。 + en-US: HertzBeat monitors common metrics for Apollo Configuration Centers.<br>You can click "<i>Create New Apollo Configuration Center</i>" to configure it, or select "<i>More Actions</i>" to import an existing configuration. <br>⚠️Note:adminservice, configservice, and portal currently require separate configuration. Simply modify the address and port. +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/apollo + en-US: https://hertzbeat.apache.org/docs/help/apollo +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + required: true + defaultValue: 8080 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + type: number + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 + +# collect metrics config list +metrics: + # Basic information indicators + - name: application_ready_time_seconds + i18n: + zh-CN: 应用启动耗时 + en-US: Application ready time + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: main_application_class + type: 1 + label: true + i18n: + zh-CN: 主应用类 + en-US: Main class + - field: metric_value + type: 0 + unit: 's' + i18n: + zh-CN: 耗时 + en-US: Time Consuming + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /prometheus + # http method: GET POST PUT DELETE PATCH + method: GET + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: prometheus + + - name: process_uptime_seconds + i18n: + zh-CN: 进程已运行时间 + en-US: Process uptime + priority: 1 + fields: + - field: metric_value + type: 0 + unit: 's' + i18n: + zh-CN: 运行时间 + en-US: uptime + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: process_cpu_usage + i18n: + zh-CN: 进程 CPU 使用率 + en-US: Process CPU usage + priority: 2 + fields: + - field: metric_value + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + aliasFields: + - metric_value + calculates: + - metric_value=metric_value*100 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: system_cpu_usage + i18n: + zh-CN: 系统 CPU 使用率 + en-US: System CPU usage + priority: 3 + fields: + - field: metric_value + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + aliasFields: + - metric_value + calculates: + - metric_value=metric_value*100 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: system_load_average_1m + i18n: + zh-CN: 系统最近一分钟平均负载 + en-US: System load average (1m) + priority: 4 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: 负载 + en-US: load + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: jvm_memory_committed_bytes + i18n: + zh-CN: JVM 已提交内存数 + en-US: JVM memory committed bytes + priority: 5 + fields: + - field: area + label: true + i18n: + zh-CN: 区域 + en-US: Area + type: 1 + - field: id + label: true + i18n: + zh-CN: 内存块Id + en-US: Memory Block Id + type: 1 + - field: metric_value + i18n: + zh-CN: JVM请求内存大小(当前) + en-US: JVM requests memory size (current) + type: 0 + unit: MB + units: + - value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: jvm_memory_used_bytes + i18n: + zh-CN: JVM 已使用内存数 + en-US: JVM memory used bytes + priority: 6 + fields: + - field: area + label: true + i18n: + zh-CN: 区域 + en-US: Area + type: 1 + - field: id + label: true + i18n: + zh-CN: 内存块Id + en-US: Memory Block Id + type: 1 + - field: metric_value + i18n: + zh-CN: JVM已使用内存大小 + en-US: JVM used memory size + type: 0 + unit: MB + units: + - value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: jvm_memory_max_bytes + i18n: + zh-CN: JVM 最大内存数 + en-US: JVM memory max bytes + priority: 7 + fields: + - field: area + label: true + i18n: + zh-CN: 区域 + en-US: Area + type: 1 + - field: id + label: true + i18n: + zh-CN: 内存块Id + en-US: Memory Block Id + type: 1 + - field: metric_value + i18n: + zh-CN: JVM 最大内存大小 + en-US: JVM used memory size + type: 0 + unit: MB + units: + - value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: jvm_gc_pause_seconds_count + i18n: + zh-CN: JVM GC 暂停次数 + en-US: Number of JVM GC pause events + priority: 8 + fields: + - field: action + label: true + type: 1 + i18n: + zh-CN: 行为 + en-US: action + - field: cause + label: true + type: 1 + i18n: + zh-CN: 原因 + en-US: cause + - field: metric_value + type: 0 + i18n: + zh-CN: 数量 + en-US: count + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: jvm_gc_pause_seconds_sum + i18n: + zh-CN: JVM GC 暂停耗时 + en-US: Total JVM GC pause time + priority: 9 + fields: + - field: action + label: true + type: 1 + i18n: + zh-CN: 行为 + en-US: action + - field: cause + label: true + type: 1 + i18n: + zh-CN: 原因 + en-US: cause + - field: metric_value + type: 0 + i18n: + zh-CN: 耗时 + en-US: time-consuming + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: jvm_memory_usage_after_gc_percent + i18n: + zh-CN: GC 执行后 JVM 内存使用率百分比 + en-US: JVM heap usage after garbage collection (%) + priority: 10 + fields: + - field: area + label: true + i18n: + zh-CN: 区域 + en-US: Area + type: 1 + - field: pool + label: true + i18n: + zh-CN: 内存池 + en-US: Heap pool + type: 1 + - field: metric_value + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + aliasFields: + - area + - pool + - metric_value + calculates: + - area + - pool + - metric_value=metric_value*100 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: process_files_max_files + i18n: + zh-CN: 允许进程打开的最大文件描述符数 + en-US: Process maximum open files + priority: 11 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: 数量 + en-US: quantity + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: process_files_open_files + i18n: + zh-CN: 进程打开的文件描述符数量 + en-US: Process open files + priority: 12 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: 数量 + en-US: quantity + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus diff --git a/hertzbeat-manager/src/main/resources/define/app-centos.yml b/hertzbeat-manager/src/main/resources/define/app-centos.yml new file mode 100644 index 0000000..8fcd437 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-centos.yml @@ -0,0 +1,766 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: centos +# The monitoring i18n name +name: + zh-CN: Centos Linux + en-US: Centos Linux + ja-JP: Centos Linux +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 Centos 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Centos</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors Centos operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New Centos</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 Centos 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Centos</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> Centosシステムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Centos Linux</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/centos/ + en-US: https://hertzbeat.apache.org/docs/help/centos/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: (uname -r ; hostname ; uptime | awk -F "," '{print $1}' | sed "s/ //g") | sed ":a;N;s/\n/^/g;ta" | awk -F '^' 'BEGIN{print "version hostname uptime"} {print $1, $2, $3}' + # ssh response data parse type: oneRow, multiRow + parseType: multiRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100-idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: "LANG=C lscpu | awk -F: '$1==\"Model name\" {print $2}';awk '/processor/{core++} END{print core}' /proc/cpuinfo;uptime | sed 's/,/ /g' | awk '{for(i=NF-2;i<=NF;i++)print $i }' | xargs;vmstat 1 1 | awk 'NR==3{print $11}';vmstat 1 1 | awk 'NR==3{print $12}';vmstat 1 2 | awk 'NR==4{print $15}'" + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: free -m | awk 'BEGIN{print "total used free buff_cache available"} NR==2{print $2,$3,$4,$6,$7}' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + en-US: Partition Num + ja-JP: パーティション + - field: block_write + type: 0 + i18n: + zh-CN: 写磁盘块数 + en-US: Block Write + ja-JP: 書き込みディスクブロック数 + - field: block_read + type: 0 + i18n: + zh-CN: 读磁盘块数 + en-US: Block Read + ja-JP: 読み取りブロック数 + - field: write_rate + type: 0 + unit: iops + i18n: + zh-CN: 磁盘写速率 + en-US: Write Rate + ja-JP: ディスク書き込み速度 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: vmstat -D | awk 'NR==1{print $1}';vmstat -D | awk 'NR==2{print $1}';vmstat 1 1 | awk 'NR==3{print $10}';vmstat 1 1 | awk 'NR==3{print $9}';vmstat 1 1 | awk 'NR==3{print $16}' + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: cat /proc/net/dev | tail -n +3 | awk 'BEGIN{ print "interface_name receive_bytes transmit_bytes"} {gsub(":", "", $1); print $1,$2,$10}' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -mP | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml b/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml new file mode 100644 index 0000000..9b727dc --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml @@ -0,0 +1,463 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: network +# The monitoring type eg: linux windows tomcat mysql aws... +app: cisco_switch +# The monitoring i18n name +name: + zh-CN: 思科通用交换机 + en-US: Cisco Switch + ja-JP: シスコ・スイッチングハブ +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 协议</a> 对 思科交换机 的通用指标(可用性,系统信息,端口流量等)进行采集监控。<br>您可以点击 “<i>新建 思科通用交换机</i>” 并进行配置SNMP相关参数添加,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP Protocol</a> to monitoring Cisco Switch general performance metrics. <br>You can click the "<i>New Cisco Switch</i>" button and config snmp params to add monitor or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 協議</a> 對 思科交換機 的通用指標(可用性,系統信息,端口流量等)進行采集監控。<br>您可以點擊 “<i>新建 思科通用交換機</i>” 並進行配置SNMP相關參數添加,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP プロトコルを介して</a> シスコ・スイッチングハブの一般的なメトリクスを監視します。<br>「<i>新規 シスコ・スイッチングハブ</i>」をクリックしてSNMPなどのパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/cisco_switch + en-US: https://hertzbeat.apache.org/docs/help/cisco_switch +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 161 + # field-param field key + - field: snmpVersion + # name-param field display i18n name + name: + zh-CN: SNMP 版本 + en-US: SNMP Version + ja-JP: SNMPバージョン + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: SNMPv1 + value: 0 + - label: SNMPv2c + value: 1 + - label: SNMPv3 + value: 3 + # field-param field key + - field: community + # name-param field display i18n name + name: + zh-CN: SNMP 团体字 + en-US: SNMP Community + ja-JP: SNMPコミュニティ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp community for v1 v2c' + # dependent parameter values list + depend: + snmpVersion: + - 0 + - 1 + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: SNMP username + en-US: SNMP username + ja-JP: SNMPユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp username for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: contextName + # name-param field display i18n name + name: + zh-CN: SNMP contextName + en-US: SNMP contextName + ja-JP: SNMPコンテキスト名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp contextName for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP authPassword + en-US: SNMP authPassword + ja-JP: SNMP認証パスワード + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPasswordEncryption + # name-param field display i18n name + name: + zh-CN: authPassword 加密方式 + en-US: authPassword Encryption + ja-JP: 認証暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: MD5 + value: 0 + - label: SHA1 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP privPassphrase + en-US: SNMP privPassphrase + ja-JP: SNMPパスワードフレーズ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPasswordEncryption + # name-param field display i18n name + name: + zh-CN: privPassword 加密方式 + en-US: privPassword Encryption + ja-JP: パスワードの暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: DES + value: 0 + - label: AES128 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: false + # hide-is hide this field and put it in advanced layout + hide: true + # default value + # 默认值 + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - system + - name: system + i18n: + zh-CN: 系统信息 + en-US: System Info + ja-JP: システム情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: name + type: 1 + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: descr + type: 1 + i18n: + zh-CN: 描述信息 + en-US: Description + ja-JP: 説明 + - field: uptime + type: 1 + i18n: + zh-CN: 运行时长 + en-US: Uptime + ja-JP: アップタイム + - field: location + type: 1 + i18n: + zh-CN: 位置 + en-US: Location + ja-JP: 位置 + - field: contact + type: 1 + i18n: + zh-CN: 联系人 + en-US: Contact + ja-JP: 連絡先 + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: snmp + # the config content when protocol is snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: get + # metrics oids: metric_name - oid_value + oids: + name: 1.3.6.1.2.1.1.5.0 + descr: 1.3.6.1.2.1.1.1.0 + uptime: 1.3.6.1.2.1.1.3.0 + location: 1.3.6.1.2.1.1.6.0 + contact: 1.3.6.1.2.1.1.4.0 + + - name: interfaces + i18n: + zh-CN: 接口详情 + en-US: Interfaces Detail + ja-JP: ネットワークカード詳細 + priority: 1 + fields: + - field: index + type: 1 + i18n: + zh-CN: 编号 + en-US: Index + ja-JP: 番号 + - field: descr + type: 1 + label: true + i18n: + zh-CN: 接口名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: mtu + type: 0 + unit: 'byte' + i18n: + zh-CN: MTU + en-US: MTU + ja-JP: MTU + - field: speed + type: 0 + unit: 'MB/s' + i18n: + zh-CN: 接口速率 + en-US: Interface Speed + ja-JP: ネットワークカード速度 + - field: in_octets + type: 0 + unit: 'MByte' + i18n: + zh-CN: 入流量 + en-US: In Octets + ja-JP: 受信されたバイト数 + - field: in_discards + type: 0 + i18n: + zh-CN: 入丢包数 + en-US: In Discards + ja-JP: 受信されたパケットのロス数 + - field: in_errors + type: 0 + i18n: + zh-CN: 入错包数 + en-US: In Errors + ja-JP: 受信異常パケット数 + - field: out_octets + type: 0 + unit: 'MByte' + i18n: + zh-CN: 出流量 + en-US: Out Octets + ja-JP: 転送されたバイト数 + - field: out_discards + type: 0 + i18n: + zh-CN: 出丢包数 + en-US: Out Discards + ja-JP: 転送されたパケットロス数 + - field: out_errors + type: 0 + i18n: + zh-CN: 出错包数 + en-US: Out Errors + ja-JP: 転送された異常パケット数 + - field: admin_status + type: 1 + i18n: + zh-CN: 配置状态 + en-US: Config Status + ja-JP: 設定ステータス + - field: oper_status + type: 1 + i18n: + zh-CN: 当前状态 + en-US: Current Status + ja-JP: 現在のステータス + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - ifIndex + - ifDescr + - ifMtu + - ifSpeed + - ifInOctets + - ifInDiscards + - ifInErrors + - ifOutOctets + - ifOutDiscards + - ifOutErrors + - ifAdminStatus + - ifOperStatus + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - index=ifIndex + - descr=ifDescr + - mtu=ifMtu + - speed=ifSpeed / 1000 / 1000 + - in_octets=ifInOctets / 1000 / 1000 + - in_discards=ifInDiscards + - in_errors=ifInErrors + - out_octets=ifOutOctets / 1000 / 1000 + - out_discards=ifOutDiscards + - out_errors=ifOutErrors + - admin_status=ifAdminStatus + - oper_status=ifOperStatus + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + authPassphrase: ^_^authPassphrase^_^ + privPassphrase: ^_^privPassphrase^_^ + privPasswordEncryption: ^_^privPasswordEncryption^_^ + authPasswordEncryption: ^_^authPasswordEncryption^_^ + contextName: ^_^contextName^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + ifIndex: 1.3.6.1.2.1.2.2.1.1 + ifDescr: 1.3.6.1.2.1.2.2.1.2 + ifMtu: 1.3.6.1.2.1.2.2.1.4 + ifSpeed: 1.3.6.1.2.1.2.2.1.5 + ifInOctets: 1.3.6.1.2.1.2.2.1.10 + ifInDiscards: 1.3.6.1.2.1.2.2.1.13 + ifInErrors: 1.3.6.1.2.1.2.2.1.14 + ifOutOctets: 1.3.6.1.2.1.2.2.1.16 + ifOutDiscards: 1.3.6.1.2.1.2.2.1.19 + ifOutErrors: 1.3.6.1.2.1.2.2.1.20 + ifAdminStatus: 1.3.6.1.2.1.2.2.1.7 + ifOperStatus: 1.3.6.1.2.1.2.2.1.8 diff --git a/hertzbeat-manager/src/main/resources/define/app-clickhouse.yml b/hertzbeat-manager/src/main/resources/define/app-clickhouse.yml new file mode 100644 index 0000000..11583df --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-clickhouse.yml @@ -0,0 +1,262 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: clickhouse +# The monitoring i18n name +name: + zh-CN: ClickHouse + en-US: ClickHouse + ja-JP: ClickHouse +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 ClickHouse 数据库监控通用指标进行测量监控。<br>您可以点击 “<i>新建 ClickHouse 数据库</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors the status codes which returned by the API. You could click the "<i>New ClickHouse Database</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 ClickHouse 資料庫監控通用名額進行量測監控。<br>您可以點擊 “<i>新建ClickHouse資料庫</i>” 並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は ClickHouse データベースの一般的なメトリクスを監視します。<br>「<i>新規 ClickHouse データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/clickhouse + en-US: https://hertzbeat.apache.org/docs/help/clickhouse +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: HTTP端口 + en-US: HTTP Port + ja-JP: HTTPポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 8123 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide-is hide this field and put it in advanced layout + hide: true + # default value + defaultValue: 6000 + # field-param field key + - field: database + # name-param field display i18n name + name: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false +# collect metrics config list +metrics: + # metrics - ping_available + - name: summary + i18n: + zh-CN: ping的可用性 + en-US: ping_available + ja-JP: サービス利用可能 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # url request interface path + url: /ping + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH + method: GET + # Whether to enable ssl/tls, that is, http or https, the default is false + ssl: false + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: website + + - name: system + priority: 1 + i18n: + zh-CN: system.metrics表的数据 + en-US: Data in the system.metrics table + ja-JP: system.metricsデータ + fields: + - field: metric + type: 1 + label: true + i18n: + zh-CN: 指标名称 + en-US: metric + ja-JP: メトリクス名 + - field: value + type: 0 + i18n: + zh-CN: 指标值 + en-US: value + ja-JP: メトリクス値 + - field: description + type: 1 + i18n: + zh-CN: 说明 + en-US: description + ja-JP: メトリクス説明 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: clickhouse + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: multiRow + sql: SELECT * FROM system.metrics where metric in ('VersionInteger','Query','Merge','Move','PartMutation','QueryThread','QueryPreempted','revision','TCPConnection','HTTPConnection','ZooKeeperWatch','EphemeralNode','contextLockWait','delayedInserts','dictCacheRequests','distributedSend','GlobalThread','GlobalThreadActive','LocalThread','LocalThreadActive','ReadonlyReplica','memoryTracking','OpenFileForRead','OpenFileForWrite','ReplicatedFetch','ReplicatedSend','ReplicatedChecks','RWLockWaitingReaders','RWLockWaitingWriters','RWLockActiveReaders','RWLockActiveWriters','storageBufferRows','StorageBufferBytes') + - name: events + priority: 2 + i18n: + zh-CN: system.events表的数据 + en-US: Data for the system.events table + ja-JP: system.eventsデータ + fields: + - field: event + type: 1 + label: true + i18n: + zh-CN: 事件信息 + en-US: event + ja-JP: イベント名 + - field: value + type: 0 + i18n: + zh-CN: 值 + en-US: value + ja-JP: 発生したイベントの数 + - field: description + type: 1 + i18n: + zh-CN: 说明 + en-US: description + ja-JP: イベントの説明 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: clickhouse + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: multiRow + sql: SELECT * FROM system.events where event in ('Query','SelectQuery','InsertQuery','InsertedRows','InsertedBytes','FailedQuery','FailedSelectQuery','FileOpen','MergeTreeDataWriterRows','MergeTreeDataWriterCompressedBytes'); + + - name: asynchronous + priority: 3 + i18n: + zh-CN: system.asynchronous_metrics表的数据 + en-US: Data from the system.asynchronous_metrics table + ja-JP: system.asynchronous_metricsデータ + fields: + - field: metric + type: 1 + label: true + i18n: + zh-CN: 指标名称 + en-US: metric + ja-JP: メトリクス名 + - field: value + type: 0 + i18n: + zh-CN: 指标值 + en-US: value + ja-JP: メトリクス値 + - field: description + type: 1 + i18n: + zh-CN: 说明 + en-US: description + ja-JP: メトリクス説明 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: clickhouse + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: multiRow + sql: SELECT * FROM system.asynchronous_metrics where metric in ('AsynchronousMetricsCalculationTimeSpent','jemalloc.arenas.all.muzzy_purged','jemalloc.arenas.all.dirty_purged','BlockReadBytes_ram1','jemalloc.background_thread.run_intervals','BlockQueueTime_nbd13','jemalloc.background_thread.num_threads','jemalloc.resident','InterserverThreads','BlockWriteMerges_nbd7','MarkCacheBytes','MarkCacheFiles','MaxPartCountForPartition'); diff --git a/hertzbeat-manager/src/main/resources/define/app-consul_sd.yml b/hertzbeat-manager/src/main/resources/define/app-consul_sd.yml new file mode 100644 index 0000000..dfca05d --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-consul_sd.yml @@ -0,0 +1,76 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: __system__ +# Monitoring application type name (consistent with file name) eg: linux windows tomcat mysql aws... +app: consul_sd +# The app api i18n name +name: + zh-CN: Consul Service Discovery + en-US: Consul Service Discovery + ja-JP: Consulサービスディスカバリー +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: __sd_host__ + # name-param field display i18n name + name: + zh-CN: Consul Host + en-US: Consul Host + ja-JP: Consulホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true + - field: __sd_port__ + name: + zh-CN: Consul 端口 + en-US: Consul Port + ja-JP: Consulポート + type: number + range: '[0,65535]' + required: true + +metrics: + - name: target + i18n: + zh-CN: 监控目标 + en-US: Monitor Target + ja-JP: 監視対象 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: host + type: 1 + i18n: + zh-CN: Host + en-US: Host + ja-JP: ホスト + - field: port + type: 1 + i18n: + zh-CN: Port + en-US: Port + ja-JP: ポート + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: consul_sd + # the config content when protocol is http_sd + consul_sd: + host: ^_^__sd_host__^_^ + port: ^_^__sd_port__^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-coreos.yml b/hertzbeat-manager/src/main/resources/define/app-coreos.yml new file mode 100644 index 0000000..3864d5e --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-coreos.yml @@ -0,0 +1,765 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: coreos +# The monitoring i18n name +name: + zh-CN: Fedora CoreOS + en-US: Fedora CoreOS + ja-JP: Fedora CoreOS +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 Fedora CoreOS 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Fedora CoreOS</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors Fedora CoreOS operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New Fedora CoreOS</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 Fedora CoreOS 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Fedora CoreOS</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH プロトコルを介して</a> Fedora CoreOSの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Fedora CoreOS</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/coreos + en-US: https://hertzbeat.apache.org/docs/help/coreos +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: (uname -r ; hostname ; uptime | awk -F "," '{print $1}' | sed "s/ //g") | sed ":a;N;s/\n/^/g;ta" | awk -F '^' 'BEGIN{print "version hostname uptime"} {print $1, $2, $3}' + # ssh response data parse type: oneRow, multiRow + parseType: multiRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100-idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: "LANG=C lscpu | awk -F: '$1==\"Model name\" {print $2}';awk '/processor/{core++} END{print core}' /proc/cpuinfo;uptime | sed 's/,/ /g' | awk '{for(i=NF-2;i<=NF;i++)print $i }' | xargs;vmstat 1 1 | awk 'NR==3{print $11}';vmstat 1 1 | awk 'NR==3{print $12}';vmstat 1 2 | awk 'NR==4{print $15}'" + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: free -m | awk 'BEGIN{print "total used free buff_cache available"} NR==2{print $2,$3,$4,$6,$7}' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + en-US: Partition Num + ja-JP: パーティション + - field: block_write + type: 0 + i18n: + zh-CN: 写磁盘块数 + en-US: Block Write + ja-JP: 書き込みディスクブロック数 + - field: block_read + type: 0 + i18n: + zh-CN: 读磁盘块数 + en-US: Block Read + ja-JP: 読み取りブロック数 + - field: write_rate + type: 0 + unit: iops + i18n: + zh-CN: 磁盘写速率 + en-US: Write Rate + ja-JP: ディスク書き込み速度 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: vmstat -D | awk 'NR==1{print $1}';vmstat -D | awk 'NR==2{print $1}';vmstat 1 1 | awk 'NR==3{print $10}';vmstat 1 1 | awk 'NR==3{print $9}';vmstat 1 1 | awk 'NR==3{print $16}' + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: cat /proc/net/dev | tail -n +3 | awk 'BEGIN{ print "interface_name receive_bytes transmit_bytes"} {print $1,$2,$10}' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -mP | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-dahua.yml b/hertzbeat-manager/src/main/resources/define/app-dahua.yml new file mode 100644 index 0000000..ca4eaaa --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-dahua.yml @@ -0,0 +1,275 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: server +# The monitoring type eg: linux windows tomcat mysql aws... +app: dahua +# The monitoring i18n name +name: + zh-CN: 大华 + en-US: Dahua + ja-JP: Dahua +# The description and help of this monitoring type +help: + zh-CN: 通过http接口监控大华设备状态,获取设备健康数据。 + en-US: Monitor Dahua devices through http interface to collect health data. + ja-JP: Hertzbeat は HTTPインターフェースを呼び出してDahuaデバイスの健全データを収集します。 + +# Input params define for monitoring(render web ui by the definition) +params: + - field: host + name: + zh-CN: 主机Host + en-US: Host + ja-JP: 目標ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + required: true + defaultValue: 80 + - field: timeout + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + type: number + range: '[1000,60000]' + required: true + defaultValue: 5000 + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + required: true + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: true + - field: ssl + name: + zh-CN: 启用HTTPS + en-US: SSL + ja-JP: SSL利用 + type: boolean + required: false + defaultValue: false + +# collect metrics config list +metrics: + - name: network_info + i18n: + zh-CN: 网络信息 + en-US: Network Info + ja-JP: ネットワーク情報 + priority: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + ssl: ^_^ssl^_^ + url: /cgi-bin/configManager.cgi?action=getConfig&name=Network + method: GET + timeout: ^_^timeout^_^ + authorization: + type: Digest Auth + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: config + fields: + - field: default_interface + type: 1 + i18n: + zh-CN: 默认网卡 + en-US: Default Interface + ja-JP: デフォルトのネットワークカード + - field: domain_name + type: 1 + i18n: + zh-CN: 域名 + en-US: Domain Name + ja-JP: ドメイン名 + - field: hostname + type: 1 + i18n: + zh-CN: 主机名 + en-US: Hostname + ja-JP: ホスト名 + - field: eth0_ip_address + type: 1 + i18n: + zh-CN: 网卡 eth0 IP地址 + en-US: Interface eth0 IP Address + ja-JP: eth0ネットワークカードのIPアドレス + - field: eth0_gateway + type: 1 + i18n: + zh-CN: 网卡 eth0 默认网关 + en-US: Interface eth0 Default Gateway + ja-JP: eth0ネットワークカードのデフォルトゲートウェイ + - field: eth0_mac_address + type: 1 + i18n: + zh-CN: 网卡 eth0 物理地址 + en-US: Interface eth0 Physical Address + ja-JP: eth0ネットワークカードの物理アドレス + - field: eth0_subnet_mask + type: 1 + i18n: + zh-CN: 网卡 eth0 子网掩码 + en-US: Interface eth0 Subnet Mask + ja-JP: eth0ネットワークカードのサブネットマスク + - field: eth0_mtu + type: 0 + i18n: + zh-CN: 网卡 eth0 MTU + en-US: Interface eth0 MTU + ja-JP: eth0ネットワークカードのMTU + - field: eth0_dns1 + type: 1 + i18n: + zh-CN: 网卡 eth0 DNS服务器1 + en-US: Interface eth0 DNS Server 1 + ja-JP: eth0ネットワークカードのDNSサーバー 1 + - field: eth0_dns2 + type: 1 + i18n: + zh-CN: 网卡 eth0 DNS服务器2 + en-US: Interface eth0 DNS Server 2 + ja-JP: eth0ネットワークカードのDNSサーバー 2 + aliasFields: + - table.Network.DefaultInterface + - table.Network.Domain + - table.Network.Hostname + - table.Network.eth0.IPAddress + - table.Network.eth0.DefaultGateway + - table.Network.eth0.PhysicalAddress + - table.Network.eth0.SubnetMask + - table.Network.eth0.MTU + - table.Network.eth0.DnsServers[0] + - table.Network.eth0.DnsServers[1] + calculates: + - default_interface=table.Network.DefaultInterface + - domain_name=table.Network.Domain + - hostname=table.Network.Hostname + - eth0_ip_address=table.Network.eth0.IPAddress + - eth0_gateway=table.Network.eth0.DefaultGateway + - eth0_mac_address=table.Network.eth0.PhysicalAddress + - eth0_subnet_mask=table.Network.eth0.SubnetMask + - eth0_mtu=table.Network.eth0.MTU + - eth0_dns1=table.Network.eth0.DnsServers[0] + - eth0_dns2=table.Network.eth0.DnsServers[1] + - name: user_info + i18n: + zh-CN: 用户信息 + en-US: User Info + ja-JP: ユーザー情報 + priority: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + ssl: ^_^ssl^_^ + url: /cgi-bin/userManager.cgi?action=getActiveUserInfoAll + method: GET + timeout: ^_^timeout^_^ + authorization: + type: Digest Auth + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: config + parseScript: users + fields: + - field: ClientAddress + type: 1 + i18n: + zh-CN: 客户端地址 + en-US: ClientAddress + ja-JP: クライアントのアドレス + - field: Name + type: 1 + i18n: + zh-CN: 客户端用户 + en-US: Name + ja-JP: ユーザー名 + - field: ClientType + type: 1 + i18n: + zh-CN: 客户端登录类型 + en-US: ClientType + ja-JP: クライアントタイプ + - field: LoginTime + type: 1 + i18n: + zh-CN: 客户端登录时间 + en-US: LoginTime + ja-JP: クライアントのログイン時間 + - name: ntp_info + i18n: + zh-CN: 校时信息 + en-US: Ntp Info + ja-JP: NTP情報 + priority: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + ssl: ^_^ssl^_^ + url: /cgi-bin/configManager.cgi?action=getConfig&name=NTP + method: GET + timeout: ^_^timeout^_^ + authorization: + type: Digest Auth + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: config + fields: + - field: ntp_address + type: 1 + i18n: + zh-CN: 校时服务器 + en-US: Ntp Address + ja-JP: NTPアドレス + - field: ntp_port + type: 1 + i18n: + zh-CN: 校时端口 + en-US: Ntp Port + ja-JP: NTPポート + - field: ntp_update_period + type: 1 + i18n: + zh-CN: 校时间隔 + en-US: Ntp Update Period + ja-JP: NTP更新期間 + aliasFields: + - table.NTP.Address + - table.NTP.Port + - table.NTP.UpdatePeriod + calculates: + - ntp_address=table.NTP.Address + - ntp_port=table.NTP.Port + - ntp_update_period=table.NTP.UpdatePeriod \ No newline at end of file diff --git a/hertzbeat-manager/src/main/resources/define/app-darwin.yml b/hertzbeat-manager/src/main/resources/define/app-darwin.yml new file mode 100644 index 0000000..31b4cb6 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-darwin.yml @@ -0,0 +1,857 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: darwin +# The monitoring i18n name +name: + zh-CN: Darwin操作系统 + en-US: OS Darwin + ja-JP: Darwinオーエス + zh-TW: Darwin操作系統 +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://HertzBeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 Darwin 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Darwin</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://HertzBeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors Darwin operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New Darwin</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://HertzBeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 Darwin 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Darwin</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: HertzBeat は <a class='help_module_content' href='https://HertzBeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> Darwinシステムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Darwin Linux</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://HertzBeat.apache.org/zh-cn/docs/help/darwin/ + en-US: https://HertzBeat.apache.org/docs/help/darwin/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + zh-TW: 目標Host + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + zh-TW: 端口 + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + zh-TW: 超時時間(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + zh-TW: 復用連接 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + zh-TW: 使用代理 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + zh-TW: 用戶名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + zh-TW: 密碼 + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + zh-TW: 私鑰 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + zh-TW: 私鑰短語 + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + zh-TW: 代理主機 + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + zh-TW: 代理端口 + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + zh-TW: 代理用戶名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + zh-TW: 代理密碼 + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + zh-TW: 代理主機私鑰 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + zh-TW: 系統基本信息 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + zh-TW: 主機名稱 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + zh-TW: 操作系統版本 + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + zh-TW: 啟動時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: | + ( + hostname | awk -F "," '{printf "%s\n", $1}' + sw_vers | awk '/:/ {printf "%s ", $2} END {print ""}' + sysctl -n kern.boottime | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}' + ) + # ssh response data parse type: oneRow, multiRow + parseType: oneRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + zh-TW: CPU 信息 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + zh-TW: 型號 + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + zh-TW: 核數 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + zh-TW: 中斷數 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + zh-TW: 負載 + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + zh-TW: 上下文切換 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + zh-TW: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100 - idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: sysctl -n machdep.cpu.brand_string; sysctl -n hw.logicalcpu; sysctl -n vm.loadavg | awk '{print $2, $3, $4}'; idle=$(top -l 1 | grep "CPU usage" | awk '{print $7}' | tr -d '%') ; free_pages=$(vm_stat | awk '/free/ {gsub(/\./,"",$3); print $3}'); active_pages=$(vm_stat | awk '/active/ {gsub(/\./,"",$3); print $3}'); first_swapins=$(vm_stat | awk '/swapins/ {gsub(/\./,"",$2); print $2}'); sleep 1; second_swapins=$(vm_stat | awk '/swapins/ {gsub(/\./,"",$2); print $2}'); echo "$free_pages"; echo "$active_pages"; echo "$idle"; + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + zh-TW: 內存信息 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + zh-TW: 總內存容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + zh-TW: 用戶程序內存量 + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + zh-TW: 空閒內存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + zh-TW: 緩存佔用內存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + zh-TW: 剩餘可用內存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + zh-TW: 內存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: | + sysctl -n hw.memsize | awk '{ + t=$1/1048576; + cmd="vm_stat"; + while (cmd | getline) { + if ($1=="Pages") { + if ($2=="free:") {f=$3} + else if ($2=="active:") {a=$3} + else if ($2=="inactive:") {i=$3} + else if ($2=="speculative:") {s=$3} + else if ($2~/wired/) {w=$NF} + }; + gsub(/\.[ \t]*$/,"",f); + gsub(/\.[ \t]*$/,"",a); + gsub(/\.[ \t]*$/,"",i); + gsub(/\.[ \t]*$/,"",s); + gsub(/\.[ \t]*$/,"",w); + }; + close(cmd); + fm=(f+s)/256; + um=(a+w)/256; + im=i/256; + bm=im; + am=fm+im; + print "total used free buff_cache available"; + printf "%d %d %d %d %d\n", t, um, fm, bm, am + }' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + zh-TW: 磁盤信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + zh-TW: 磁盤總數 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + zh-TW: 分區總數 + en-US: Partition Num + ja-JP: パーティション + # - field: block_write + # type: 0 + # i18n: + # zh-CN: 写磁盘块数 + # zh-TW: 寫磁盤塊數 + # en-US: Block Write + # ja-JP: 書き込みディスクブロック数 + # - field: block_read + # type: 0 + # i18n: + # zh-CN: 读磁盘块数 + # zh-TW: 讀磁盤塊數 + # en-US: Block Read + # ja-JP: 読み取りブロック数 + # - field: write_rate + # type: 0 + # unit: iops + # i18n: + # zh-CN: 磁盘写速率 + # zh-TW: 磁盤寫速率 + # en-US: Write Rate + # ja-JP: ディスク書き込み速度 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # script: cat /proc/net/dev | tail -n +3 | awk 'BEGIN{ print "interface_name receive_bytes transmit_bytes"} {gsub(":", "", $1); print $1,$2,$10}' + script: diskutil list | grep '^/dev/' | wc -l; diskutil list | grep -E 'Apple_HFS|Apple_APFS|Microsoft Basic Data|Linux Filesystem' | wc -l; + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + zh-TW: 網卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + zh-TW: 網卡名稱 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + zh-TW: 入站數據流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + zh-TW: 出站數據流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: netstat -i | grep -v "lo0" | awk 'BEGIN { print "interface_name receive_bytes transmit_bytes" } NR>1 && $1 !~ /^-/ { print $1, $5, $9 }' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + zh-TW: 文件系統 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + zh-TW: 檔案系統 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + zh-TW: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + zh-TW: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + zh-TW: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + zh-TW: 掛載點 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -mP | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + zh-TW: Top10 CPU 進程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + zh-TW: 進程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + zh-TW: CPU佔用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + zh-TW: 內存佔用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + zh-TW: 執行指令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + zh-TW: Top10 內存進程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + zh-TW: 進程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + zh-TW: 內存佔用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + zh-TW: CPU佔用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + zh-TW: 執行指令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-db2.yml b/hertzbeat-manager/src/main/resources/define/app-db2.yml new file mode 100644 index 0000000..b40b814 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-db2.yml @@ -0,0 +1,381 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: db2 +# The monitoring i18n name +name: + zh-CN: DB2数据库 + en-US: DB2 DB + ja-JP: DB2 データベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 DB2 数据库的通用性能指标(表空间、性能状态、用户连接、缓存、慢SQL等)进行采集监控。<br>您可以点击“<i>新建 DB2 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC Protocol</a> to configure SQL for collecting general metrics of DB2 database (tablespace、user connect、performance、slow SQL etc.). <br>You can click "<i>New DB2 Database</i>" and configure it, or select "<i>More Operations"</i> to import the existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 協議</a> 通過配置 SQL 對 DB2 數據庫的通用性能指標(表空間、性能狀態、用戶連接、緩存、慢SQL等)進行采集監控。<br>您可以點擊“<i>新建 DB2 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBCプロトコルを介して</a> DB2 データベースの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 DB2 データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/db2/ + en-US: https://hertzbeat.apache.org/docs/help/db2/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 50000 # DB2 default port + - field: timeout + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + type: number + range: '[400,200000]' + required: false + hide: true + defaultValue: 6000 + - field: reuseConnection + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + type: boolean + required: false + defaultValue: true + - field: database + name: + zh-CN: 数据库名 + en-US: Database Name + ja-JP: データベース名 + type: text + required: true # Database Name is typically mandatory for DB2 JDBC connection + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + - field: url + name: + zh-CN: URL + en-US: URL + ja-JP: URL + type: text + required: false + hide: true + +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 基本信息 + en-US: Basic Info + ja-JP: 基礎情報 + priority: 0 + fields: + - field: database_version + type: 1 + label: true + i18n: + zh-CN: 数据库版本 + en-US: Database Version + ja-JP: データベースバージョン + - field: instance_name + type: 1 + i18n: + zh-CN: 实例名 + en-US: Instance Name + ja-JP: インスタンス名 + - field: status + type: 1 + i18n: + zh-CN: 实例状态 + en-US: Instance Status + ja-JP: インスタンス的状态 + - field: num_db + type: 0 + i18n: + zh-CN: 数据库数量 + en-US: Number of Databases + ja-JP: データベースの数 + aliasFields: + - INSTANCE_NAME + - INSTANCE_STATUS + - NUM_DB + - CURRENT_USER + calculates: + - database_version=INSTANCE_NAME + - instance_name=INSTANCE_NAME + - status=INSTANCE_STATUS + - num_db=NUM_DB + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: db2 + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: oneRow + sql: "SELECT CURRENT SERVER AS INSTANCE_NAME, 'ACTIVE' AS INSTANCE_STATUS, 1 AS NUM_DB, CURRENT USER AS CURRENT_USER FROM SYSIBM.SYSDUMMY1" + url: ^_^url^_^ + + # Combined tablespace capacity and usage + - name: tablespace_usage + i18n: + zh-CN: 表空间使用率 + en-US: Tablespace Usage + ja-JP: 表領域使用率 + priority: 1 + fields: + - field: tablespace_name + type: 1 + i18n: + zh-CN: 表空间名 + en-US: Tablespace Name + ja-JP: 表領域名 + label: true + - field: status + type: 1 + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: タイプ + - field: total + type: 0 + i18n: + zh-CN: 总容量 + en-US: Total Size + ja-JP: 総容量 + unit: MB + - field: used + type: 0 + i18n: + zh-CN: 已使用 + en-US: Used Size + ja-JP: 使用済み + unit: MB + - field: free + type: 0 + i18n: + zh-CN: 空闲 + en-US: Free Size + ja-JP: 利用可能 + unit: MB + - field: used_percentage + type: 0 + i18n: + zh-CN: 已用百分比 + en-US: Used Percentage + ja-JP: 使用した比率 + unit: '%' + aliasFields: + - TBSP_NAME + - TBSP_TYPE + - TBSP_TOTAL_SIZE_KB + - TBSP_USED_SIZE_KB + - TBSP_FREE_SIZE_KB + calculates: + - tablespace_name=TBSP_NAME + - status=TBSP_TYPE + - total=TBSP_TOTAL_SIZE_KB / 1024 + - used=TBSP_USED_SIZE_KB / 1024 + - free=TBSP_FREE_SIZE_KB / 1024 + - used_percentage=TBSP_USED_SIZE_KB / TBSP_TOTAL_SIZE_KB * 100 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: db2 + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: "SELECT TBSP_NAME, TBSP_TYPE, TBSP_TOTAL_SIZE_KB, TBSP_USED_SIZE_KB, TBSP_FREE_SIZE_KB FROM SYSIBMADM.TBSP_UTILIZATION" + url: ^_^url^_^ + + # Sessions/Applications status distribution + - name: sessions_status + i18n: + zh-CN: 会话状态 + en-US: Session Status + ja-JP: セッション状態 + priority: 2 + fields: + - field: status_type + type: 1 + i18n: + zh-CN: 状态类型 + en-US: Status Type + ja-JP: 状態タイプ + label: true + - field: count + type: 0 + i18n: + zh-CN: 数量 + en-US: Count + ja-JP: 数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: db2 + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: "SELECT APPL_STATUS AS STATUS_TYPE, COUNT(*) AS COUNT FROM SYSIBMADM.APPLICATIONS GROUP BY APPL_STATUS" + url: ^_^url^_^ + + + # Total number of applications/sessions + - name: process + i18n: + zh-CN: 应用进程数 + en-US: Application Process Count + ja-JP: アプリケーションプロセス数 + priority: 5 + fields: + - field: process_count + type: 0 + i18n: + zh-CN: 进程数 + en-US: Process Count + ja-JP: プロセス数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: db2 + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: oneRow + # Count of all running applications + sql: "select count(*) as process_count from SYSIBMADM.APPLICATIONS" + url: ^_^url^_^ + + - name: locks + i18n: + zh-CN: 锁 + en-US: Locks + ja-JP: ロック + priority: 10 + fields: + - field: waiting_locks + type: 0 + i18n: + zh-CN: 当前等待锁数 + en-US: Current Waiting Locks + ja-JP: 現在の待機中ロック数 + aliasFields: + - WAITING_LOCKS + calculates: + - waiting_locks=WAITING_LOCKS + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: db2 + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: oneRow + sql: "SELECT COUNT(*) AS WAITING_LOCKS FROM SYSIBMADM.MON_LOCKWAITS" + url: ^_^url^_^ + + # Slow Query / Top Statements (by Total CPU Time) + - name: slow_query + i18n: + zh-CN: 慢查询 + en-US: Slow Query + ja-JP: スロークエリ + priority: 14 + fields: + - field: avg_exe_time + type: 0 + i18n: + zh-CN: 平均执行时间 + en-US: Avg Execution Time + ja-JP: 平均実行時間 + unit: ms + - field: sql_text + type: 1 + i18n: + zh-CN: SQL文本 + en-US: SQL Text + ja-JP: SQLテキスト + aliasFields: + - AVG_EXE_TIME + - SQL_TEXT + calculates: + - avg_exe_time=AVG_EXE_TIME + - sql_text=SQL_TEXT + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: db2 + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: "select total_act_time/num_exec_with_metrics as AVG_EXE_TIME, substr(stmt_text,1,4000) as SQL_TEXT from table(mon_get_pkg_cache_stmt(null, null, null, -2)) as T where num_exec_with_metrics>=1 order by 1 desc fetch first 100 rows only" + url: ^_^url^_^ \ No newline at end of file diff --git a/hertzbeat-manager/src/main/resources/define/app-debian.yml b/hertzbeat-manager/src/main/resources/define/app-debian.yml new file mode 100644 index 0000000..674ca55 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-debian.yml @@ -0,0 +1,764 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: debian +# The monitoring i18n name +name: + zh-CN: Debian操作系统 + en-US: Debian Linux + ja-JP: Debian Linux +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 Debian 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Debian</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors Debian operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New Debian</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 Debian 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Debian</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> Centosシステムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Debian</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/debian + en-US: https://hertzbeat.apache.org/docs/help/debian +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: (uname -r ; hostname ; uptime | awk -F "," '{print $1}' | sed "s/ //g") | sed ":a;N;s/\n/^/g;ta" | awk -F '^' 'BEGIN{print "version hostname uptime"} {print $1, $2, $3}' + # ssh response data parse type: oneRow, multiRow + parseType: multiRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100-idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: "LANG=C lscpu | awk -F: '$1==\"Model name\" {print $2}';awk '/processor/{core++} END{print core}' /proc/cpuinfo;uptime | sed 's/,/ /g' | awk '{for(i=NF-2;i<=NF;i++)print $i }' | xargs;vmstat 1 1 | awk 'NR==3{print $11}';vmstat 1 1 | awk 'NR==3{print $12}';vmstat 1 2 | awk 'NR==4{print $15}'" + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: free -m | awk 'BEGIN{print "total used free buff_cache available"} NR==2{print $2,$3,$4,$6,$7}' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + en-US: Partition Num + ja-JP: パーティション + - field: block_write + type: 0 + i18n: + zh-CN: 写磁盘块数 + en-US: Block Write + ja-JP: 書き込みディスクブロック数 + - field: block_read + type: 0 + i18n: + zh-CN: 读磁盘块数 + en-US: Block Read + ja-JP: 読み取りブロック数 + - field: write_rate + type: 0 + unit: iops + i18n: + zh-CN: 磁盘写速率 + en-US: Write Rate + ja-JP: ディスク書き込み速度 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: vmstat -D | awk 'NR==1{print $1}';vmstat -D | awk 'NR==2{print $1}';vmstat 1 1 | awk 'NR==3{print $10}';vmstat 1 1 | awk 'NR==3{print $9}';vmstat 1 1 | awk 'NR==3{print $16}' + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: cat /proc/net/dev | tail -n +3 | awk 'BEGIN{ print "interface_name receive_bytes transmit_bytes"} {print $1,$2,$10}' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -mP | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps --no-headers -eo pid,%cpu,%mem,cmd --sort=-%cpu | head -n 10 | awk 'BEGIN {print "pid cpu_usage mem_usage command"} {print $1, $2, $3, $4}' + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps --no-headers -eo pid,%cpu,%mem,cmd --sort=-%mem | head -n 10 | awk 'BEGIN {print "pid cpu_usage mem_usage command"} {print $1, $2, $3, $4}' + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-deepseek.yml b/hertzbeat-manager/src/main/resources/define/app-deepseek.yml new file mode 100644 index 0000000..31391a2 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-deepseek.yml @@ -0,0 +1,108 @@ +# 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. + + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: llm +# The monitoring type eg: linux windows tomcat mysql aws... +app: deepseek +# The monitoring i18n name +name: + zh-CN: Deepseek + en-US: Deepseek + ja-JP: Deepseek +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Deepseek Api进行测量监控。<br>您可以点击 “<i>新建 Deepseek</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors Deepseek Api. You could click the "<i>New Deepseek</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat對Deepseek Api進行量測監控。<br>您可以點擊“<i>Deepseek</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は Deepseek Apiを監視します。<br>「<i>新規 Deepseek</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/deepseek + en-US: https://hertzbeat.apache.org/docs/help/deepseek +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: token + # name-param field display i18n name + name: + zh-CN: 会话密钥 + en-US: Session Key + ja-JP: セッション鍵 + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true + +# collect metrics config list +metrics: + # metrics - auth + - name: billing + i18n: + zh-CN: 计费 + en-US: Billing + ja-JP: 料金 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: currency + type: 1 + i18n: + zh-CN: 货币 + en-US: Currency + ja-JP: 通貨 + - field: total_balance + type: 0 + i18n: + zh-CN: 可用余额 + en-US: Available Balance + ja-JP: 利用可能残高 + - field: granted_balance + type: 0 + i18n: + zh-CN: 未过期的赠金余额 + en-US: Unexpired Gift Balance + ja-JP: 有効期限内のギフト残高 + - field: topped_up_balance + type: 0 + i18n: + zh-CN: 充值的余额 + en-US: Topped Up Balance + ja-JP: 残高 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: api.deepseek.com + # http port + port: 443 + # http url + url: /user/balance + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: true + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Bearer Token + bearerTokenToken: ^_^token^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$.balance_infos.*' diff --git a/hertzbeat-manager/src/main/resources/define/app-dm.yml b/hertzbeat-manager/src/main/resources/define/app-dm.yml new file mode 100644 index 0000000..cf6bd8b --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-dm.yml @@ -0,0 +1,243 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: dm +# The monitoring i18n name +name: + zh-CN: 达梦数据库 + en-US: DM DB + ja-JP: DM データベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 DM达梦数据库 的通用性能指标(basic、status、thread)进行采集监控,支持版本为 DM8+。<br>您可以点击“<i>新建 达梦数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat monitors DM database of basic performance metrics such as status and thread, the version we support is DM8+. You could click the "<i>New DM</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: HertzBeat 對 DM達夢數據庫 的通用性能指標(basic、status、thread)進行采集監控,支持版本爲 DM8+。<br>您可以點擊“<i>新建 達夢數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は DM データベースの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 DM データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/dm + en-US: https://hertzbeat.apache.org/docs/help/dm +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 5236 + # param field input placeholder + placeholder: 'Please input port' + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 用户密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间 + en-US: Timeout + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: true + # default value + defaultValue: 3000 +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 基本信息 + en-US: Basic Info + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: PORT_NUM + i18n: + zh-CN: 端口号 + en-US: Port Number + ja-JP: ポート + type: 1 + - field: CTL_PATH + i18n: + zh-CN: 控制路径 + en-US: Control Path + ja-JP: 制御パス + type: 1 + - field: MAX_SESSIONS + i18n: + zh-CN: 最大会话数 + en-US: Max Sessions + ja-JP: 最大セッション数 + type: 0 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - PORT_NUM + - CTL_PATH + - MAX_SESSIONS + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - PORT_NUM=PORT_NUM + - CTL_PATH=CTL_PATH + - MAX_SESSIONS=MAX_SESSIONS + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: dm + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + # script sql + sql: select PARA_NAME, PARA_VALUE from SYS."V$DM_INI" where PARA_NAME = 'MAX_SESSIONS'or PARA_NAME = 'CTL_PATH' or PARA_NAME = 'PORT_NUM'; + # JDBC url + url: ^_^url^_^ + + - name: status + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + priority: 1 + fields: + - field: status + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + type: 1 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: dm + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: oneRow + sql: SELECT status$ as status FROM v$instance; + url: ^_^url^_^ + + + - name: thread + i18n: + zh-CN: 线程 + en-US: Thread + ja-JP: スレッド情報 + priority: 2 + fields: + - field: dm_sql_thd + i18n: + zh-CN: SQL线程数 + en-US: SQL Thread + ja-JP: SQLスレッド数 + type: 0 + - field: dm_io_thd + i18n: + zh-CN: I/O线程数 + en-US: I/O Thread + ja-JP: I/Oスレッド数 + type: 0 + - field: dm_quit_thd + i18n: + zh-CN: 退出线程数 + en-US: Quit Thread + ja-JP: 終了したスレッド数 + type: 0 + aliasFields: + - dm_sql_thd + - dm_io_thd + - dm_quit_thd + calculates: + - dm_sql_thd=dm_sql_thd + - dm_io_thd=dm_io_thd + - dm_quit_thd=dm_quit_thd + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: dm + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + sql: SELECT DISTINCT NAME, COUNT(*) AS NUM FROM V$THREADS GROUP BY NAME ORDER BY NUM DESC; + url: ^_^url^_^ + + diff --git a/hertzbeat-manager/src/main/resources/define/app-dns.yml b/hertzbeat-manager/src/main/resources/define/app-dns.yml new file mode 100644 index 0000000..9277c11 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-dns.yml @@ -0,0 +1,528 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: dns +# The monitoring i18n name +name: + zh-CN: DNS服务器监控 + en-US: DNS SERVER MONITORS + ja-JP: DNS サーバーモニター +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 DNS 服务相关指标进行监测。 + en-US: HertzBeat monitors related indicators of the DNS service. + zh-TW: HertzBeat對DNS服務相關名額進行監測。 + ja-JP: HertzBeatはDNSのメトリクスを監視します。 +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: DNS服务器IP + en-US: DNS Server IP + ja-JP: DNS サーバーのIP + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value 53 + defaultValue: 53 + - field: address + # name-param field display i18n name + name: + zh-CN: 域名解析的地址 + en-US: Address For DNS + ja-JP: アドレス + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 + - field: tcp + # name-param field display i18n name + name: + zh-CN: 是否使用tcp协议 + en-US: Use TCP Protocol + ja-JP: TCP プロトコルを利用 + # type-param field type(boolean mapping the html switch tag) + type: boolean + # required-true or false + required: true + # default value false + # 默认值 false + defaultValue: false + - field: recordType + # name-param field display i18n name + name: + zh-CN: 记录类型 + en-US: Record Type + ja-JP: レコードタイプ + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: SRV + value: SRV + - label: A + value: A + - label: AAAA + value: AAAA + - label: MX + value: MX + - label: NS + value: NS + - label: ANY + value: ANY + - field: queryClass + # name-param field display i18n name + name: + zh-CN: 查询类别 + en-US: Query Class + ja-JP: クエリクラス + type: radio + required: true + options: + - label: IN + value: IN + - label: CHAOS + value: CHAOS + - label: HESIOD + value: HESIOD + - label: NONE + value: NONE + - label: ANY + value: ANY + # default value IN + defaultValue: IN + hide: true +# collect metrics config list +metrics: + # metrics - header + - name: header + i18n: + zh-CN: Header + en-US: Header + ja-JP: ヘッダ + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: opcode + type: 1 + i18n: + zh-CN: 操作码 + en-US: Opcode + ja-JP: 命令コード + - field: status + type: 1 + i18n: + zh-CN: 响应状态 + en-US: Response Status + ja-JP: 応答ステータス + - field: flags + type: 1 + i18n: + zh-CN: 响应标志 + en-US: Response Flags + ja-JP: 応答フラグ + - field: questionRowCount + type: 0 + i18n: + zh-CN: 请求记录数 + en-US: Question Record Count + ja-JP: Questionレコード数 + - field: answerRowCount + type: 0 + i18n: + zh-CN: 响应记录数 + en-US: Answer Record Count + ja-JP: Answerレコード数 + - field: authorityRowCount + type: 0 + i18n: + zh-CN: 授权记录数 + en-US: Authority Record Count + ja-JP: Authorityレコード数 + - field: additionalRowCount + type: 0 + i18n: + zh-CN: 附加记录数 + en-US: Additional Record Count + ja-JP: Additionalレコード数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, dns + protocol: dns + # Specific collection configuration when protocol is telnet protocol + dns: + # DNS Server IP + dnsServerIP: ^_^host^_^ + # port + port: ^_^port^_^ + # address that used to run dig command + address: ^_^address^_^ + # timeout + timeout: ^_^timeout^_^ + # Use TCP Protocol + tcp: ^_^tcp^_^ + # Query Class + queryClass: ^_^queryClass^_^ + # Record Type + recordType: ^_^recordType^_^ + + # metrics - question + - name: question + i18n: + zh-CN: Question + en-US: Question + ja-JP: Question + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: section + type: 1 + i18n: + zh-CN: Section + en-US: Section + ja-JP: Section + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, dns + protocol: dns + # Specific collection configuration when protocol is telnet protocol + dns: + # DNS Server IP + dnsServerIP: ^_^host^_^ + # port + port: ^_^port^_^ + # address that used to run dig command + address: ^_^address^_^ + # timeout + timeout: ^_^timeout^_^ + # Use TCP Protocol + tcp: ^_^tcp^_^ + # Query Class + queryClass: ^_^queryClass^_^ + # Record Type + recordType: ^_^recordType^_^ + + # metrics - answer + - name: answer + i18n: + zh-CN: Answer + en-US: Answer + ja-JP: Answer + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: section0 + type: 1 + i18n: + zh-CN: Section0 + en-US: Section0 + ja-JP: Section0 + - field: section1 + type: 1 + i18n: + zh-CN: Section1 + en-US: Section1 + ja-JP: Section1 + - field: section2 + type: 1 + i18n: + zh-CN: Section2 + en-US: Section2 + ja-JP: Section2 + - field: section3 + type: 1 + i18n: + zh-CN: Section3 + en-US: Section3 + ja-JP: Section3 + - field: section4 + type: 1 + i18n: + zh-CN: Section4 + en-US: Section4 + ja-JP: Section4 + - field: section5 + type: 1 + i18n: + zh-CN: Section5 + en-US: Section5 + ja-JP: Section5 + - field: section6 + type: 1 + i18n: + zh-CN: Section6 + en-US: Section6 + ja-JP: Section6 + - field: section7 + type: 1 + i18n: + zh-CN: Section7 + en-US: Section7 + ja-JP: Section7 + - field: section8 + type: 1 + i18n: + zh-CN: Section8 + en-US: Section8 + ja-JP: Section8 + - field: section9 + type: 1 + i18n: + zh-CN: Section9 + en-US: Section9 + ja-JP: Section9 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, dns + protocol: dns + # Specific collection configuration when protocol is telnet protocol + dns: + # DNS Server IP + dnsServerIP: ^_^host^_^ + # port + port: ^_^port^_^ + # address that used to run dig command + address: ^_^address^_^ + # timeout + timeout: ^_^timeout^_^ + # Use TCP Protocol + tcp: ^_^tcp^_^ + # Query Class + queryClass: ^_^queryClass^_^ + # Record Type + recordType: ^_^recordType^_^ + + # metrics - authority + - name: authority + i18n: + zh-CN: Authority + en-US: Authority + ja-JP: Authority + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: section0 + type: 1 + i18n: + zh-CN: Section0 + en-US: Section0 + ja-JP: Section0 + - field: section1 + type: 1 + i18n: + zh-CN: Section1 + en-US: Section1 + ja-JP: Section1 + - field: section2 + type: 1 + i18n: + zh-CN: Section2 + en-US: Section2 + ja-JP: Section2 + - field: section3 + type: 1 + i18n: + zh-CN: Section3 + en-US: Section3 + ja-JP: Section3 + - field: section4 + type: 1 + i18n: + zh-CN: Section4 + en-US: Section4 + ja-JP: Section4 + - field: section5 + type: 1 + i18n: + zh-CN: Section5 + en-US: Section5 + ja-JP: Section5 + - field: section6 + type: 1 + i18n: + zh-CN: Section6 + en-US: Section6 + ja-JP: Section6 + - field: section7 + type: 1 + i18n: + zh-CN: Section7 + en-US: Section7 + ja-JP: Section7 + - field: section8 + type: 1 + i18n: + zh-CN: Section8 + en-US: Section8 + ja-JP: Section8 + - field: section9 + type: 1 + i18n: + zh-CN: Section9 + en-US: Section9 + ja-JP: Section9 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, dns + protocol: dns + # Specific collection configuration when protocol is telnet protocol + dns: + # DNS Server IP + dnsServerIP: ^_^host^_^ + # port + port: ^_^port^_^ + # address that used to run dig command + address: ^_^address^_^ + # timeout + timeout: ^_^timeout^_^ + # Use TCP Protocol + tcp: ^_^tcp^_^ + # Query Class + queryClass: ^_^queryClass^_^ + # Record Type + recordType: ^_^recordType^_^ + + # metrics - additional + - name: additional + i18n: + zh-CN: Additional + en-US: Additional + ja-JP: Additional + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 4 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: section0 + type: 1 + i18n: + zh-CN: Section0 + en-US: Section0 + ja-JP: Section0 + - field: section1 + type: 1 + i18n: + zh-CN: Section1 + en-US: Section1 + ja-JP: Section1 + - field: section2 + type: 1 + i18n: + zh-CN: Section2 + en-US: Section2 + ja-JP: Section2 + - field: section3 + type: 1 + i18n: + zh-CN: Section3 + en-US: Section3 + ja-JP: Section3 + - field: section4 + type: 1 + i18n: + zh-CN: Section4 + en-US: Section4 + ja-JP: Section4 + - field: section5 + type: 1 + i18n: + zh-CN: Section5 + en-US: Section5 + ja-JP: Section5 + - field: section6 + type: 1 + i18n: + zh-CN: Section6 + en-US: Section6 + ja-JP: Section6 + - field: section7 + type: 1 + i18n: + zh-CN: Section7 + en-US: Section7 + ja-JP: Section7 + - field: section8 + type: 1 + i18n: + zh-CN: Section8 + en-US: Section8 + ja-JP: Section8 + - field: section9 + type: 1 + i18n: + zh-CN: Section9 + en-US: Section9 + ja-JP: Section9 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, dns + protocol: dns + # Specific collection configuration when protocol is telnet protocol + dns: + # DNS Server IP + dnsServerIP: ^_^host^_^ + # port + port: ^_^port^_^ + # address that used to run dig command + address: ^_^address^_^ + # timeout + timeout: ^_^timeout^_^ + # Use TCP Protocol + tcp: ^_^tcp^_^ + # Query Class + queryClass: ^_^queryClass^_^ + # Record Type + recordType: ^_^recordType^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-dns_sd.yml b/hertzbeat-manager/src/main/resources/define/app-dns_sd.yml new file mode 100644 index 0000000..7ed6de3 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-dns_sd.yml @@ -0,0 +1,114 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: __system__ +# Monitoring application type name (consistent with file name) eg: linux windows tomcat mysql aws... +app: dns_sd +# The app api i18n name +name: + zh-CN: Dns Service Discovery + en-US: Dns Service Discovery + ja-JP: Dns サービスディスカバリー +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: __sd_host__ + # name-param field display i18n name + name: + zh-CN: DNS地址 + en-US: DNS Host + ja-JP: DNS ホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true + # field-param field key + - field: __sd_port__ + # name-param field display i18n name + name: + zh-CN: DNS端口 + en-US: DNS Port + ja-JP: DNS ポート + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true + # default value + defaultValue: 53 + - field: __sd_record_type__ + # name-param field display i18n name + name: + zh-CN: 记录类型 + en-US: Record Type + ja-JP: レコードタイプ + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: SRV + value: 33 + - label: A + value: 1 + - label: AAAA + value: 28 + - label: MX + value: 15 + - label: NS + value: 2 + - field: __sd_record_name__ + # name-param field display i18n name + name: + zh-CN: 记录名 + en-US: Record Name + ja-JP: レコード名 + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true +metrics: + - name: target + i18n: + zh-CN: 监控目标 + en-US: Monitor Target + ja-JP: 監視対象 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: host + type: 1 + i18n: + zh-CN: Host + en-US: Host + ja-JP: ホスト + - field: port + type: 1 + i18n: + zh-CN: Port + en-US: Port + ja-JP: ポート + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: dns_sd + # the config content when protocol is dns_sd + dns_sd: + host: ^_^__sd_host__^_^ + port: ^_^__sd_port__^_^ + recordType: ^_^__sd_record_type__^_^ + recordName: ^_^__sd_record_name__^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-docker.yml b/hertzbeat-manager/src/main/resources/define/app-docker.yml new file mode 100644 index 0000000..c5ac989 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-docker.yml @@ -0,0 +1,364 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: cn +# The monitoring type eg: linux windows tomcat mysql aws... +app: docker +# The monitoring i18n name +name: + zh-CN: Docker + en-US: Docker + ja-JP: Docker +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 Docker 容器的通用性能指标(system、containers、stats)进行采集监控。<br><span class='help_module_span'>注意⚠️:为了监控 Docker 中的容器信息,您需要打开端口,让采集请求获取到对应的信息, <a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/docker'>点击查看开启步骤</a>。</span> + en-US: HertzBeat monitoring Docker of general performance metrics such as containers, status etc. <br><span class='help_module_span'>Note⚠️:In order to monitoring metrics of Docker, you need to enable the data export port so that the collector can collect data from here,<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/docker'>Click here to view the steps</a>.</span> + zh-TW: HertzBeat 對 Docker 容器的通用性能指標(system、containers、stats)進行采集監控。<br><span class='help_module_span'>注意⚠️:爲了監控 Docker 中的容器信息,您需要打開端口,讓采集請求獲取到對應的信息, <a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/docker'>點擊查看開啓步驟</a>。</span> + ja-JP: HertzBeat は Dockerコンテナの一般的なパフォーマンスのメトリクスを監視します。<br><span class='help_module_span'>注意⚠️:Dockerコンテナの情報を監視するために、ポートを有効にする必要があります。<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/docker'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/docker/ + en-US: https://hertzbeat.apache.org/docs/help/docker/ +# Input params define for monitoring(render web ui by the definition) +params: + # field - field name identifier + - field: host + # name - parameter field display name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-field type, style (most map input tag type attribute) + type: host + # Is it mandatory? true-required false-optional + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + # When type is number, use range to represent the range + range: '[0,65535]' + required: true + defaultValue: 2375 + - field: ssl + name: + zh-CN: SSL连接 + en-US: SSL + ja-JP: SSL利用 + # shen the type is boolean, the front end uses switch to display the switch + type: boolean + required: false +metrics: + - name: system + i18n: + zh-CN: 系统 + en-US: system + ja-JP: システム + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # metrics fields list + fields: + # metric information includes field name type field type: 0-number, 1-string whether instance is the primary key of the instance unit: metric unit + - field: name + i18n: + zh-CN: 主机名 + en-US: name + ja-JP: ホスト名 + type: 1 + - field: version + i18n: + zh-CN: 版本 + en-US: version + ja-JP: バージョン + type: 1 + - field: os + i18n: + zh-CN: 操作系统 + en-US: os + ja-JP: オーエス + type: 1 + - field: root_dir + i18n: + zh-CN: 根目录 + en-US: root dir + ja-JP: ルートディレクトリ + type: 1 + - field: containers + i18n: + zh-CN: 容器总数 + en-US: containers + ja-JP: コンテナ数 + type: 0 + - field: containers_running + i18n: + zh-CN: 运行容器数 + en-US: running containers + ja-JP: 実行中のコンテナ数 + type: 0 + - field: containers_paused + i18n: + zh-CN: 暂停容器数 + en-US: paused containers + ja-JP: 一時停止中のコンテナ数 + type: 0 + - field: containers_stopped + i18n: + zh-CN: 已停止容器数 + en-US: stopped containers + ja-JP: 停止したコンテナの数 + type: 0 + - field: images + i18n: + zh-CN: 镜像数 + en-US: images + ja-JP: イメージ数 + type: 0 + - field: ncpu + i18n: + zh-CN: CPU数量 + en-US: ncpu + ja-JP: CPU数 + type: 0 + - field: mem_total + i18n: + zh-CN: 内存总量 + en-US: total memory + ja-JP: メモリ容量 + type: 0 + unit: MB + - field: system_time + i18n: + zh-CN: 系统时间 + en-US: system time + ja-JP: システム時間 + type: 1 + aliasFields: + - Name + - ServerVersion + - OperatingSystem + - OSType + - Architecture + - DockerRootDir + - Containers + - ContainersRunning + - ContainersPaused + - ContainersStopped + - Images + - NCPU + - MemTotal + - SystemTime + calculates: + - name = Name + - version = OperatingSystem + " " + ServerVersion + - os = OSType + " " + Architecture + - root_dir = DockerRootDir + - containers = Containers + - containers_running = ContainersRunning + - containers_paused = ContainersPaused + - containers_stopped = ContainersStopped + - images = Images + - ncpu = NCPU + - mem_total = MemTotal + - system_time = SystemTime + units: + - mem_total=B->MB + protocol: http + # When the protocol is the http protocol, the specific collection configuration + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: /info + # request method GET POST PUT DELETE PATCH + method: GET + # Whether to enable ssl/tls, that is, http or https, the default is false + ssl: ^_^ssl^_^ + # response data analysis method: default-system rules, jsonPath-jsonPath script, website-api usability metric monitoring + parseType: default + + - name: containers + i18n: + zh-CN: 容器总数 + en-US: containers + ja-JP: コンテナ数 + # The smaller the priority value (0-127), the higher the priority of the metrics. Lower-priority metricss will be scheduled after higher-priority metricss have been collected. metricss with the same priority will be scheduled to collect data in parallel. + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # metrics fields list + fields: + # metric information includes field name type field type: 0-number, 1-string whether instance is the primary key of the instance unit: metric unit + - field: id + i18n: + zh-CN: 主键 + en-US: ID + ja-JP: 主キー + type: 1 + - field: name + i18n: + zh-CN: 名称 + en-US: name + ja-JP: 名前 + type: 1 + - field: image + i18n: + zh-CN: 镜像 + en-US: image + ja-JP: イメージ + type: 1 + - field: command + i18n: + zh-CN: 命令行 + en-US: command + ja-JP: コマンド + type: 1 + - field: state + i18n: + zh-CN: 状态 + en-US: state + ja-JP: 状態 + type: 1 + - field: status + i18n: + zh-CN: 状态 + en-US: status + ja-JP: 健全状態 + type: 1 + aliasFields: + - Id + - $.Names[0] + - Image + - Command + - State + - Status + calculates: + - id = Id + - name=$.Names[0] + - image = Image + - command = Command + - state = State + - status = Status + # Protocols used for monitoring and collection eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # When the protocol is the http protocol, the specific collection configuration + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: /containers/json?all=true + # request method GET POST PUT DELETE PATCH + method: GET + # Whether to enable ssl/tls, that is, http or https, the default is false + ssl: ^_^ssl^_^ + # Response data analysis method: default-system rules, jsonPath-jsonPath script, website-api usability metric monitoring + parseType: jsonPath + parseScript: '$.*' + + - name: stats + i18n: + zh-CN: 统计 + en-US: stats + ja-JP: 統計 + priority: 2 + fields: + - field: name + i18n: + zh-CN: 名称 + en-US: name + ja-JP: 名前 + type: 1 + - field: available_memory + i18n: + zh-CN: 可用内存 + en-US: available memory + ja-JP: 使用可能のメモリ + type: 0 + unit: MB + - field: used_memory + i18n: + zh-CN: 已用内存 + en-US: used memory + ja-JP: 使用したメモリ + type: 0 + unit: MB + - field: memory_usage + i18n: + zh-CN: 内存使用率 + en-US: memory usage + ja-JP: メモリ使用率 + type: 0 + unit: '%' + - field: cpu_delta + i18n: + zh-CN: 已使用CPU数 + en-US: cpu delta + ja-JP: 使用中のCPUコア数 + type: 0 + - field: number_cpus + i18n: + zh-CN: 可使用CPU数 + en-US: cpus number + ja-JP: 使用可能のCPUコア数 + type: 0 + - field: cpu_usage + i18n: + zh-CN: CPU使用率 + en-US: cpu usage + ja-JP: CPU使用率 + type: 0 + unit: '%' + aliasFields: + - $.name + - $.memory_stats.usage + - $.memory_stats.limit + - $.cpu_stats.cpu_usage.total_usage + - $.precpu_stats.cpu_usage.total_usage + - $.cpu_stats.online_cpus + - $.cpu_stats.system_cpu_usage + - $.precpu_stats.system_cpu_usage + calculates: + - name=$.name + - available_memory = $.memory_stats.limit + - used_memory=$.memory_stats.usage + - memory_usage=($.memory_stats.usage / $.memory_stats.limit) * 100 + - cpu_delta=$.cpu_stats.cpu_usage.total_usage - $.precpu_stats.cpu_usage.total_usage + - number_cpus=$.cpu_stats.online_cpus + - cpu_usage=(($.cpu_stats.cpu_usage.total_usage - $.precpu_stats.cpu_usage.total_usage) / ($.cpu_stats.system_cpu_usage - $.precpu_stats.system_cpu_usage)) * $.cpu_stats.online_cpus * 100 + units: + - available_memory=B->MB + - used_memory=B->MB + protocol: http + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: /containers/^o^id^o^/stats + # request method GET POST PUT DELETE PATCH + method: GET + # Whether to enable ssl/tls, that is, http or https, the default is false + ssl: ^_^ssl^_^ + params: + stream: false + # Response data analysis method: default-system rules, jsonPath-jsonPath script, website-api usability metric monitoring + parseType: jsonPath + parseScript: '$' diff --git a/hertzbeat-manager/src/main/resources/define/app-dolphinscheduler.yml b/hertzbeat-manager/src/main/resources/define/app-dolphinscheduler.yml new file mode 100644 index 0000000..37dadfd --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-dolphinscheduler.yml @@ -0,0 +1,619 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: dolphinscheduler +# The monitoring i18n name +name: + zh-CN: Apache DolphinScheduler + en-US: Apache DolphinScheduler + ja-JP: Apache DolphinScheduler +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Apache DolphinScheduler (支持v3.3.0或更高版本)通用指标进行测量监控。<br>您可以点击 “<i>新建 Apache DolphinScheduler</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat measures and monitors the general metrics of Apache DolphinScheduler (support v3.3.0 or later).<br>You can click on “<i>Create New Apache DolphinScheduler</i>” to configure it, or select “<i>More Actions</i>” to import an existing configuration. + ja-JP: Hertzbeat は Apache DolphinScheduler(v3.3.0+)の一般的なメトリクスを監視します。<br>「<i>新規 Apache DolphinScheduler</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/dolphinscheduler + en-US: https://hertzbeat.apache.org/docs/help/dolphinscheduler +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 12345 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启用HTTPS + en-US: SSL + ja-JP: SSL + # type-param field type(boolean mapping the html h tag) + type: boolean + # required-true or false + required: true + - field: token + # name-param field display i18n name + name: + zh-CN: 令牌 + en-US: Token + ja-JP: トークン + type: text + limit: 100 + required: true + +# collect metrics config list +metrics: + - name: master + i18n: + zh-CN: Master + en-US: Master + ja-JP: マスター情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: host + type: 1 + i18n: + zh-CN: 主机地址 + en-US: Host + ja-JP: ホスト + - field: port + type: 1 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: serverStatus + type: 1 + i18n: + zh-CN: 状态 + en-US: Server Status + ja-JP: サーバーステータス + - field: processId + type: 1 + i18n: + zh-CN: 进程 ID + en-US: Process Id + ja-JP: プロセスID + - field: runningTime + type: 0 + i18n: + zh-CN: 运行时间 + en-US: Up Time + ja-JP: アップタイム + - field: cpuUsage + type: 0 + unit: '%' + i18n: + zh-CN: 处理器使用量 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: memoryUsage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用量 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: diskUsage + type: 0 + unit: '%' + i18n: + zh-CN: 磁盘使用量 + en-US: Disk Usage + ja-JP: ディスク使用率 + - field: jvmCpuUsage + type: 0 + unit: '%' + i18n: + zh-CN: JVM CPU 使用量 + en-US: JVM CPU Usage + ja-JP: Java仮想マシンのCPU使用率 + - field: jvmMemoryUsage + type: 0 + unit: '%' + i18n: + zh-CN: JVM 内存 使用量 + en-US: JVM Memory Usage + ja-JP: Java仮想マシンのメモリ使用率 + - field: jvmHeapUsed + type: 0 + i18n: + zh-CN: JVM 已使用的堆内存大小 + en-US: JVM Heap Used + ja-JP: Java仮想マシンが使用したヒープメモリのサイズ + - field: jvmNonHeapUsed + type: 0 + i18n: + zh-CN: JVM 已使用的非堆内存大小 + en-US: JVM NonHeap Used + ja-JP: Java仮想マシンが使用したノンヒープメモリのサイズ + - field: jvmHeapMax + type: 0 + i18n: + zh-CN: JVM 配置的最大堆内存大小 + en-US: JVM Heap Max + ja-JP: Java仮想マシンのヒープメモリの最大値 + - field: jvmNonHeapMax + type: 0 + i18n: + zh-CN: JVM 配置的最大非堆内存大小 + en-US: JVM NonHeap Max + ja-JP: Java仮想マシンのノンヒープメモリの最大値 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.heartBeatInfo + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - host=json:apply($.heartBeatInfo).host + - port=json:apply($.heartBeatInfo).port + - serverStatus=json:apply($.heartBeatInfo).serverStatus + - processId=json:apply($.heartBeatInfo).processId + - runningTime=(now()-json:apply($.heartBeatInfo).startupTime)/86400000 + - cpuUsage=json:apply($.heartBeatInfo).cpuUsage * 100 + - memoryUsage=json:apply($.heartBeatInfo).memoryUsage * 100 + - diskUsage=json:apply($.heartBeatInfo).diskUsage * 100 + - jvmCpuUsage=json:apply($.heartBeatInfo).jvmCpuUsage + - jvmMemoryUsage=json:apply($.heartBeatInfo).jvmMemoryUsage + - jvmHeapUsed=json:apply($.heartBeatInfo).jvmHeapUsed + - jvmNonHeapUsed=json:apply($.heartBeatInfo).jvmNonHeapUsed + - jvmHeapMax=json:apply($.heartBeatInfo).jvmHeapMax + - jvmNonHeapMax=json:apply($.heartBeatInfo).jvmNonHeapMax + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /dolphinscheduler/monitor/MASTER + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + # http request header content + headers: + token: ^_^token^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$.data[*]' + + - name: worker + i18n: + zh-CN: Worker + en-US: Worker + ja-JP: ワーカー情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: host + type: 1 + i18n: + zh-CN: 主机地址 + en-US: Host + ja-JP: ホスト + - field: port + type: 1 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: serverStatus + type: 1 + i18n: + zh-CN: 状态 + en-US: Server Status + ja-JP: サーバーステータス + - field: processId + type: 1 + i18n: + zh-CN: 进程 ID + en-US: Process Id + ja-JP: プロセスID + - field: runningTime + type: 0 + i18n: + zh-CN: 运行时间 + en-US: Up Time + ja-JP: アップタイム + - field: cpuUsage + type: 0 + unit: '%' + i18n: + zh-CN: 处理器使用量 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: memoryUsage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用量 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: diskUsage + type: 0 + unit: '%' + i18n: + zh-CN: 磁盘使用量 + en-US: Disk Usage + ja-JP: ディスク使用率 + - field: jvmCpuUsage + type: 0 + unit: '%' + i18n: + zh-CN: JVM CPU 使用量 + en-US: JVM CPU Usage + ja-JP: Java仮想マシンのCPU使用率 + - field: jvmMemoryUsage + type: 0 + unit: '%' + i18n: + zh-CN: JVM 内存 使用量 + en-US: JVM Memory Usage + ja-JP: Java仮想マシンのメモリ使用率 + - field: jvmHeapUsed + type: 0 + i18n: + zh-CN: JVM 已使用的堆内存大小 + en-US: JVM Heap Used + ja-JP: Java仮想マシンが使用したヒープメモリのサイズ + - field: jvmNonHeapUsed + type: 0 + i18n: + zh-CN: JVM 已使用的非堆内存大小 + en-US: JVM NonHeap Used + ja-JP: Java仮想マシンが使用したノンヒープメモリのサイズ + - field: jvmHeapMax + type: 0 + i18n: + zh-CN: JVM 配置的最大堆内存大小 + en-US: JVM Heap Max + ja-JP: Java仮想マシンのヒープメモリの最大値 + - field: jvmNonHeapMax + type: 0 + i18n: + zh-CN: JVM 配置的最大非堆内存大小 + en-US: JVM NonHeap Max + ja-JP: Java仮想マシンのノンヒープメモリの最大値 + - field: workerHostWeight + type: 0 + i18n: + zh-CN: 权重 + en-US: Weight + ja-JP: ウェイト + - field: threadPoolUsage + type: 0 + unit: '%' + i18n: + zh-CN: 线程池使用量 + en-US: Thread Pool Usage + ja-JP: スレッドプールの使用率 + - field: workerGroup + type: 1 + i18n: + zh-CN: Worker 组 + en-US: Worker Group + ja-JP: ワーカーグループ + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.heartBeatInfo + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - host=json:apply($.heartBeatInfo).host + - port=json:apply($.heartBeatInfo).port + - serverStatus=json:apply($.heartBeatInfo).serverStatus + - processId=json:apply($.heartBeatInfo).processId + - runningTime=(now()-json:apply($.heartBeatInfo).startupTime)/86400000 + - cpuUsage=json:apply($.heartBeatInfo).cpuUsage * 100 + - memoryUsage=json:apply($.heartBeatInfo).memoryUsage * 100 + - diskUsage=json:apply($.heartBeatInfo).diskUsage * 100 + - jvmCpuUsage=json:apply($.heartBeatInfo).jvmCpuUsage + - jvmMemoryUsage=json:apply($.heartBeatInfo).jvmMemoryUsage + - jvmHeapUsed=json:apply($.heartBeatInfo).jvmHeapUsed + - jvmNonHeapUsed=json:apply($.heartBeatInfo).jvmNonHeapUsed + - jvmHeapMax=json:apply($.heartBeatInfo).jvmHeapMax + - jvmNonHeapMax=json:apply($.heartBeatInfo).jvmNonHeapMax + - workerHostWeight=json:apply($.heartBeatInfo).workerHostWeight + - threadPoolUsage=json:apply($.heartBeatInfo).threadPoolUsage + - workerGroup=json:apply($.heartBeatInfo).workerGroup + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /dolphinscheduler/monitor/WORKER + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + # http request header content + headers: + token: ^_^token^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$.data[*]' + + - name: alert-server + i18n: + zh-CN: Alert Server + en-US: Alert Server + ja-JP: アラートサーバーの情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: host + type: 1 + i18n: + zh-CN: 主机地址 + en-US: Host + ja-JP: ホスト + - field: port + type: 1 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: serverStatus + type: 1 + i18n: + zh-CN: 状态 + en-US: Server Status + ja-JP: サーバーステータス + - field: processId + type: 1 + i18n: + zh-CN: 进程 ID + en-US: Process Id + ja-JP: プロセスID + - field: runningTime + type: 0 + i18n: + zh-CN: 运行时间 + en-US: Up Time + ja-JP: アップタイム + - field: cpuUsage + type: 0 + unit: '%' + i18n: + zh-CN: 处理器使用量 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: memoryUsage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用量 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: diskUsage + type: 0 + unit: '%' + i18n: + zh-CN: 磁盘使用量 + en-US: Disk Usage + ja-JP: ディスク使用率 + - field: jvmCpuUsage + type: 0 + unit: '%' + i18n: + zh-CN: JVM CPU 使用量 + en-US: JVM CPU Usage + ja-JP: Java仮想マシンのCPU使用率 + - field: jvmMemoryUsage + type: 0 + unit: '%' + i18n: + zh-CN: JVM 内存 使用量 + en-US: JVM Memory Usage + ja-JP: Java仮想マシンのメモリ使用率 + - field: jvmHeapUsed + type: 0 + i18n: + zh-CN: JVM 已使用的堆内存大小 + en-US: JVM Heap Used + ja-JP: Java仮想マシンが使用したヒープメモリのサイズ + - field: jvmNonHeapUsed + type: 0 + i18n: + zh-CN: JVM 已使用的非堆内存大小 + en-US: JVM NonHeap Used + ja-JP: Java仮想マシンが使用したノンヒープメモリのサイズ + - field: jvmHeapMax + type: 0 + i18n: + zh-CN: JVM 配置的最大堆内存大小 + en-US: JVM Heap Max + ja-JP: Java仮想マシンのヒープメモリの最大値 + - field: jvmNonHeapMax + type: 0 + i18n: + zh-CN: JVM 配置的最大非堆内存大小 + en-US: JVM NonHeap Max + ja-JP: Java仮想マシンのノンヒープメモリの最大値 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.heartBeatInfo + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - host=json:apply($.heartBeatInfo).host + - port=json:apply($.heartBeatInfo).port + - serverStatus=json:apply($.heartBeatInfo).serverStatus + - processId=json:apply($.heartBeatInfo).processId + - runningTime=(now()-json:apply($.heartBeatInfo).startupTime)/86400000 + - cpuUsage=json:apply($.heartBeatInfo).cpuUsage * 100 + - memoryUsage=json:apply($.heartBeatInfo).memoryUsage * 100 + - diskUsage=json:apply($.heartBeatInfo).diskUsage * 100 + - jvmCpuUsage=json:apply($.heartBeatInfo).jvmCpuUsage + - jvmMemoryUsage=json:apply($.heartBeatInfo).jvmMemoryUsage + - jvmHeapUsed=json:apply($.heartBeatInfo).jvmHeapUsed + - jvmNonHeapUsed=json:apply($.heartBeatInfo).jvmNonHeapUsed + - jvmHeapMax=json:apply($.heartBeatInfo).jvmHeapMax + - jvmNonHeapMax=json:apply($.heartBeatInfo).jvmNonHeapMax + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /dolphinscheduler/monitor/ALERT_SERVER + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + # http request header content + headers: + token: ^_^token^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$.data[*]' + + - name: database + i18n: + zh-CN: 数据库 + en-US: Database + ja-JP: データベース情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: dbType + type: 1 + i18n: + zh-CN: 数据库类型 + en-US: Database Type + ja-JP: データベースのタイプ + - field: state + type: 1 + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + - field: maxConnections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + - field: threadsConnections + type: 0 + i18n: + zh-CN: 当前连接数 + en-US: Threads Connections + ja-JP: 現在接続数 + - field: threadsRunningConnections + type: 0 + i18n: + zh-CN: 当前活跃连接数 + en-US: Threads Running Connections + ja-JP: 現在のアクティブな接続数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - dbType + - state + - maxConnections + - threadsConnections + - threadsRunningConnections + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - dbType=dbType + - state=state + - maxConnections=maxConnections + - threadsConnections=threadsConnections + - threadsRunningConnections=threadsRunningConnections + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /dolphinscheduler/monitor/databases + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + # http request header content + headers: + token: ^_^token^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$.data[0]' diff --git a/hertzbeat-manager/src/main/resources/define/app-doris_be.yml b/hertzbeat-manager/src/main/resources/define/app-doris_be.yml new file mode 100644 index 0000000..5f5f958 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-doris_be.yml @@ -0,0 +1,512 @@ +# 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. + +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: doris_be +# The monitoring i18n name +name: + zh-CN: Apache Doris BE + en-US: Apache Doris BE + ja-JP: Apache Doris BE +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 DORIS 数据库BE的通用性能指标(load channel count、memtable flush total、process thread num、light work max threads等)进行采集监控,支持版本为DORIS2.0.0。<br>您可以点击 “<i>新建 Doris DatabaseBE</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitoring Doris DatabaseBE through general performance metric such as load channel count, memtable flush total, process thread num and light work max threads. The version we support is DORIS2.0.0. You could click the "<i>New Doris DatabaseBE Monitor</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 DORIS 資料庫BE的通用性能指標(load channel count、memtable flush total、process thread num、light work max threads等)進行採集監控,支持版本為DORIS2.0.0。<br>您可以點擊“<i>新建Doris DatabaseBE</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は DORIS 2.0.0のデータベースBEの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Doris DatabaseBE</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/doris_be/ + en-US: https://hertzbeat.apache.org/docs/help/doris_be/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标 Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # default value + defaultValue: 8040 + required: true + - field: timeout + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + type: number + required: false + # hide param-true or false + hide: true + defaultValue: 6000 +metrics: + - name: doris_be_load_channel_count + i18n: + zh-CN: 加载通道数 + en-US: Load Channel Count + ja-JP: ロードチャンネル数 + priority: 0 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_memtable_flush_total + i18n: + zh-CN: Memtable 刷新总数 + en-US: Memtable Flush Total + ja-JP: Memtableのリフレッシュ回数 + priority: 1 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_plan_fragment_count + i18n: + zh-CN: Plan Fragment 数量 + en-US: Plan Fragment Count + ja-JP: Plan Fragment数 + priority: 2 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_process_thread_num + i18n: + zh-CN: 进程线程数 + en-US: Process Thread Num + ja-JP: プロセススレッド数 + priority: 3 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_query_scan_rows + i18n: + zh-CN: 查询扫描行数 + en-US: Query Scan Rows + ja-JP: クエリのスキャン行数 + priority: 4 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_result_buffer_block_count + i18n: + zh-CN: 结果缓冲区块数 + en-US: Result Buffer Block Count + ja-JP: 結果バッファブロック数 + priority: 5 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_send_batch_thread_pool_queue_size + i18n: + zh-CN: 批量发送线程池队列大小 + en-US: Send Batch Thread Pool Queue Size + ja-JP: バッチ転送されたスレッドプールのキューサイズ + priority: 6 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_tablet_base_max_compaction_score + i18n: + zh-CN: Tablet Base 最大压缩分数 + en-US: Tablet Base Max Compaction Score + ja-JP: Tablet Base最大圧縮スコア + priority: 7 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_timeout_canceled_fragment_count + i18n: + zh-CN: 超时取消的 Fragment 数量 + en-US: Timeout Canceled Fragment Count + ja-JP: タイムアウトでキャンセルされたFragment数 + priority: 8 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_load_rows + i18n: + zh-CN: 加载行数 + en-US: Load Rows + ja-JP: ロード行数 + priority: 9 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_all_rowsets_num + i18n: + zh-CN: Rowset 总数 + en-US: All Rowsets Num + ja-JP: Rowset数 + priority: 10 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_all_segments_num + i18n: + zh-CN: Segment 总数 + en-US: All Segments Num + ja-JP: Segment数 + priority: 11 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_heavy_work_max_threads + i18n: + zh-CN: 繁重工作最大线程数 + en-US: Heavy Work Max Threads + ja-JP: 重作業用最大スレッド数 + priority: 12 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_light_work_max_threads + i18n: + zh-CN: 轻量工作最大线程数 + en-US: Light Work Max Threads + ja-JP: 軽作業用最大スレッド数 + priority: 13 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_heavy_work_pool_queue_size + i18n: + zh-CN: 繁重工作池队列大小 + en-US: Heavy Work Pool Queue Size + ja-JP: 重作業用プールのキューサイズ + priority: 14 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_light_work_pool_queue_size + i18n: + zh-CN: 轻量工作池队列大小 + en-US: Light Work Pool Queue Size + ja-JP: 軽作業用プールのキューサイズ + priority: 15 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_heavy_work_active_threads + i18n: + zh-CN: 繁重工作活跃线程数 + en-US: Heavy Work Active Threads + ja-JP: 活動中の重作業用スレッド数 + priority: 16 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_light_work_active_threads + i18n: + zh-CN: 轻量工作活跃线程数 + en-US: Light Work Active Threads + ja-JP: 活動中の軽作業用スレッド数 + priority: 17 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_compaction_bytes_total + i18n: + zh-CN: 压缩字节总数 + en-US: Compaction Bytes Total + ja-JP: 圧縮バイト合計 + priority: 18 + fields: + - field: type + type: 1 + label: true + - field: value + type: 0 + unit: Byte + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_disks_avail_capacity + i18n: + zh-CN: 磁盘可用容量 + en-US: Disks Avail Capacity + ja-JP: 利用可能なディスク容量 + priority: 19 + fields: + - field: path + type: 1 + label: true + - field: value + type: 0 + unit: Byte + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_disks_total_capacity + i18n: + zh-CN: 磁盘总容量 + en-US: Disks Total Capacity + ja-JP: ディスク容量 + priority: 20 + fields: + - field: path + type: 1 + label: true + - field: value + type: 0 + unit: Byte + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_local_bytes_read_total + i18n: + zh-CN: 本地读取字节总数 + en-US: Local Bytes Read Total + ja-JP: 読み取られたバイト数 + priority: 21 + fields: + - field: value + type: 0 + label: true + unit: Byte + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_local_bytes_written_total + i18n: + zh-CN: 本地写入字节总数 + en-US: Local Bytes Written Total + ja-JP: 書き込まれたバイト数 + priority: 22 + fields: + - field: value + type: 0 + label: true + unit: Byte + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_be_memory_allocated_bytes + i18n: + zh-CN: 内存分配字节 + en-US: Memory Allocated Bytes + ja-JP: メモリ割り当てバイト + priority: 23 + fields: + - field: value + type: 0 + label: true + unit: Byte + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus diff --git a/hertzbeat-manager/src/main/resources/define/app-doris_fe.yml b/hertzbeat-manager/src/main/resources/define/app-doris_fe.yml new file mode 100644 index 0000000..c6024b4 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-doris_fe.yml @@ -0,0 +1,402 @@ +# 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. + +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: doris_fe +# The monitoring i18n name +name: + zh-CN: Apache Doris FE + en-US: Apache Doris FE + ja-JP: Apache Doris FE +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Doris 数据库FE的通用指标(doris_fe connection total、doris_fe edit log clean、doris_fe image、doris_fe rps等)进行测量监控,支持版本为DORIS2.0.0。<br>您可以点击 “<i>新建 Doris DatabaseFE</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitoring Doris DatabaseFE through general performance metric such as doris_fe connection total, doris_fe edit log clean, doris_fe image and doris_fe rps. The version we support is DORIS2.0.0. You could click the "<i>New Doris DatabaseFE Monitor</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 Doris 資料庫FE的通用名額(doris_fe connection total、doris_fe edit log clean、doris_fe image、doris_fe rps等)進行量測監控,支持版本為DORIS2.0.0。<br>您可以點擊“<i>新建Doris DatabaseFE</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は DORIS 2.0.0のデータベースFEの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Doris DatabaseFE</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/doris_fe/ + en-US: https://hertzbeat.apache.org/docs/help/doris_fe/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标 Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # default value + defaultValue: 8030 + required: true + - field: timeout + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + type: number + required: false + # hide param-true or false + hide: true + defaultValue: 6000 +metrics: + - name: doris_fe_connection_total + i18n: + zh-CN: 连接总数 + en-US: Connection Total + ja-JP: 接続総数 + priority: 0 + fields: + - field: value + type: 0 + label: true + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_fe_edit_log_clean + i18n: + zh-CN: 编辑日志清理 + en-US: Edit Log Clean + ja-JP: 編集ログのクリーン + priority: 1 + fields: + - field: type + type: 1 + label: true + - field: value + type: 0 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_fe_edit_log + i18n: + zh-CN: 编辑日志 + en-US: Edit Log + ja-JP: 編集ログ + priority: 2 + fields: + - field: type + type: 1 + label: true + - field: value + type: 0 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_fe_image_clean + i18n: + zh-CN: 图片清理 + en-US: Image Clean + ja-JP: イメージのクリーン + priority: 3 + fields: + - field: type + type: 1 + label: true + - field: value + type: 0 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_fe_image_write + i18n: + zh-CN: 图片写入 + en-US: Image Write + ja-JP: イメージを書き込む + priority: 4 + fields: + - field: type + type: 1 + label: true + - field: value + type: 0 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_fe_query_err + i18n: + zh-CN: 查询错误 + en-US: Query Error + ja-JP: クエリエラー + priority: 5 + fields: + - field: value + type: 0 + label: true + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_fe_max_journal_id + i18n: + zh-CN: 最大日志ID + en-US: Max Journal ID + ja-JP: マックスジャーナルID + priority: 6 + fields: + - field: value + type: 0 + label: true + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_fe_max_tablet_compaction_score + i18n: + zh-CN: 最大Tablet压缩分数 + en-US: Max Tablet Compaction Score + ja-JP: Tablet最大圧縮スコア + priority: 7 + fields: + - field: value + type: 0 + label: true + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_fe_qps + i18n: + zh-CN: 每秒查询率 + en-US: QPS + ja-JP: QPS + priority: 8 + fields: + - field: value + type: 0 + label: true + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_fe_query_err_rate + i18n: + zh-CN: 查询错误率 + en-US: Query Error Rate + ja-JP: クエリエラー率 + priority: 9 + fields: + - field: value + type: 0 + label: true + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # 主机host: ipv4 ipv6 域名 + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: doris_fe_report_queue_size + i18n: + zh-CN: 报告队列大小 + en-US: Report Queue Size + ja-JP: レポートのキューサイズ + priority: 10 + fields: + - field: value + type: 0 + label: true + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_fe_rps + i18n: + zh-CN: 每秒转数 + en-US: RPS + ja-JP: RPS + priority: 11 + fields: + - field: value + type: 0 + label: true + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_fe_scheduled_tablet_num + i18n: + zh-CN: 调度 Tablet 数量 + en-US: Scheduled Tablet Num + ja-JP: Tablet予定数 + priority: 12 + fields: + - field: value + type: 0 + label: true + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: doris_fe_txn_status + i18n: + zh-CN: 事务状态 + en-US: Transaction Status + ja-JP: トランザクション状態 + priority: 13 + fields: + - field: type + type: 1 + label: true + - field: value + type: 0 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus diff --git a/hertzbeat-manager/src/main/resources/define/app-dynamic_tp.yml b/hertzbeat-manager/src/main/resources/define/app-dynamic_tp.yml new file mode 100644 index 0000000..b85626b --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-dynamic_tp.yml @@ -0,0 +1,310 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: program +# The monitoring type eg: linux windows tomcat mysql aws... +app: dynamic_tp +# The monitoring i18n name +name: + zh-CN: DynamicTp 线程池 + en-US: DynamicTp Pool + ja-JP: DynamicTpスレッドプール +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 DynamicTp actuator 暴露的线程池性能指标(thread pool)进行采集监控。<br><span class='help_module_span'>注意⚠️:您需要集成使用 DynamicTp,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/dynamic_tp'>点击查看集成步骤</a>。</span> + en-US: HertzBeat monitoring DynamicTp of the thread Pool Performance Metrics which exposed by DynamicTp actuator. <br><span class='help_module_span'>Note⚠️:You should integrate and use DynamicTp, <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/dynamic_tp'>Click here to view the specific steps.</a></span> + zh-TW: HertzBeat 對 DynamicTp actuator暴露的執行緒池性能指標(thread pool)進行採集監控。<br><span class='help_module_span'>注意⚠️:您需要集成使用DynamicTp,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/dynamic_tp'>點擊查看集成步驟</a>。</span> + ja-JP: HertzBeat は DynamicTpスレッドプールの一般的なパフォーマンスのメトリクスを監視します。<br><span class='help_module_span'>注意⚠️:DynamicTpの使用を統合する必要があり、<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/dynamic_tp'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/dynamic_tp/ + en-US: https://hertzbeat.apache.org/docs/help/dynamic_tp/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 8080 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: false + # field-param field key + - field: base_path + # name-param field display i18n name + name: + zh-CN: Base Path + en-US: Base Path + ja-JP: Base Path + # type-param field type(most mapping the html input type) + type: text + # default value + defaultValue: /actuator + # required-true or false + required: true + # hide-is hide this field and put it in advanced layout + hide: true +# collect metrics config list +metrics: + # metrics - thread_pool + - name: thread_pool + i18n: + zh-CN: 线程池 + en-US: thread pool + ja-JP: スレッドプール + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: pool_name + i18n: + zh-CN: 池名称 + en-US: pool name + ja-JP: プール名 + type: 1 + label: true + - field: queue_type + i18n: + zh-CN: 队列类型 + en-US: queue type + ja-JP: キュータイプ + type: 1 + - field: core_pool_size + i18n: + zh-CN: 核心线程数 + en-US: core pool size + ja-JP: コアのスレッド数 + type: 0 + - field: maximum_pool_size + i18n: + zh-CN: 最大线程数 + en-US: maximum pool size + ja-JP: 最大スレッド数 + type: 0 + - field: fair + i18n: + zh-CN: 公平 + en-US: fair + ja-JP: 公平 + type: 1 + - field: reject_handler_name + type: 1 + i18n: + zh-CN: 拒绝策略 + en-US: reject handler name + ja-JP: 拒否戦略 + - field: dynamic + i18n: + zh-CN: 动态 + en-US: dynamic + ja-JP: dynamic + type: 1 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - poolName + - corePoolSize + - maximumPoolSize + - queueType + - fair + - rejectHandlerName + - dynamic + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - pool_name=poolName + - core_pool_size=corePoolSize + - maximum_pool_size=maximumPoolSize + - queue_type=queueType + - fair=fair + - reject_handler_name=rejectHandlerName + - dynamic=dynamic + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # url request interface path + url: ^_^base_path^_^/dynamic-tp + # http method: GET POST PUT DELETE PATCH + method: GET + # Whether to enable ssl/tls, that is, http or https, the default is false + ssl: ^_^ssl^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + # http response data parse script + parseScript: '$.*' + + - name: thread_pool_running + i18n: + zh-CN: 运行中的线程池 + en-US: thread pool running + ja-JP: 実行中のスレッドプール + priority: 1 + fields: + - field: pool_name + i18n: + zh-CN: 池名称 + en-US: pool name + ja-JP: プール名 + type: 1 + label: true + - field: queue_capacity + i18n: + zh-CN: 队列容量 + en-US: queue capacity + ja-JP: キュー容量 + type: 0 + unit: MB + - field: queue_size + i18n: + zh-CN: 队列大小 + en-US: queue size + ja-JP: キューサイズ + type: 0 + - field: queue_remaining_capacity + i18n: + zh-CN: 队列剩余容量 + en-US: queue remaining capacity + ja-JP: 使用可能のキュー容量 + type: 0 + unit: MB + - field: active_count + i18n: + zh-CN: 活动线程数 + en-US: active count + ja-JP: 活動中のスレッド数 + type: 0 + - field: task_count + i18n: + zh-CN: 任务数 + en-US: task count + ja-JP: タスク数 + type: 0 + - field: completed_task_count + i18n: + zh-CN: 完成的任务数 + en-US: completed task count + ja-JP: 完了したタスク数 + type: 0 + - field: largest_pool_size + i18n: + zh-CN: 最大线程数 + en-US: largest pool size + ja-JP: 最大スレッド数 + type: 0 + - field: pool_size + i18n: + zh-CN: 线程池大小 + en-US: pool size + ja-JP: プールサイズ + type: 0 + - field: wait_task_count + i18n: + zh-CN: 等待任务数 + en-US: wait task count + ja-JP: 待機中のタスク数 + type: 0 + - field: reject_count + i18n: + zh-CN: 拒绝数 + en-US: reject count + ja-JP: 拒否数 + type: 0 + - field: run_timeout_count + i18n: + zh-CN: 运行超时数 + en-US: run timeout count + ja-JP: 実行タイムアウト数 + type: 0 + - field: queue_timeout_count + i18n: + zh-CN: 队列超时数 + en-US: queue timeout count + ja-JP: キュータイムアウト数 + type: 0 + aliasFields: + - poolName + - queueCapacity + - queueSize + - queueRemainingCapacity + - activeCount + - taskCount + - completedTaskCount + - largestPoolSize + - poolSize + - waitTaskCount + - rejectCount + - runTimeoutCount + - queueTimeoutCount + calculates: + - pool_name=poolName + - queue_capacity=queueCapacity + - queue_size=queueSize + - queue_remaining_capacity=queueRemainingCapacity + - active_count=activeCount + - task_count=taskCount + - completed_task_count=completedTaskCount + - largest_pool_size=largestPoolSize + - pool_size=poolSize + - wait_task_count=waitTaskCount + - reject_count=rejectCount + - run_timeout_count=runTimeoutCount + - queue_timeout_count=queueTimeoutCount + units: + - queue_capacity=B->MB + - queue_remaining_capacity=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/dynamic-tp + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.*' diff --git a/hertzbeat-manager/src/main/resources/define/app-elasticsearch.yml b/hertzbeat-manager/src/main/resources/define/app-elasticsearch.yml new file mode 100644 index 0000000..48475d7 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-elasticsearch.yml @@ -0,0 +1,407 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: elasticsearch +# The monitoring i18n name +name: + zh-CN: ElasticSearch + en-US: ElasticSearch + ja-JP: ElasticSearch +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 ElasticSearch 数据库监控通用指标进行测量监控。<br>您可以点击 “<i>新建 ElasticSearch</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitoring ElasticSearch through general performance metrics. You could click the "<i>New ElasticSearch</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 ElasticSearch 資料庫監控通用名額進行量測監控。<br>您可以點擊“<i>新建ElasticSearch</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: HertzBeat は ElasticSearch データベースの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 ElasticSearch</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/elasticsearch + en-US: https://hertzbeat.apache.org/docs/help/elasticsearch +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 9200 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启用HTTPS + en-US: SSL + ja-JP: SSL利用 + # type-param field type(boolean mapping the html h tag) + type: boolean + # required-true or false + required: true + # field-param field key + - field: authType + # name-param field display i18n name + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + # type-param field type(most mapping the html input tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false +# collect metrics config list +metrics: + # metrics - cpu + - name: health + i18n: + zh-CN: 集群健康状态 + en-US: Cluster Health Status + ja-JP: クラスタ健全状態 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: cluster_name + type: 1 + label: true + i18n: + zh-CN: 集群名称 + en-US: Cluster Name + ja-JP: クラスタ名 + - field: status + type: 1 + i18n: + zh-CN: 集群状态 + en-US: status + ja-JP: 状態 + - field: nodes + type: 0 + i18n: + zh-CN: 集群节点数 + en-US: nodes + ja-JP: ノード数 + - field: data_nodes + type: 0 + i18n: + zh-CN: 数据节点数 + en-US: Data Nodes + ja-JP: データノード数 + - field: active_primary_shards + type: 0 + i18n: + zh-CN: 主节点活跃分片数 + en-US: Active Primary Shards + ja-JP: 主ノードの活動中シャーズ数 + - field: active_shards + type: 0 + i18n: + zh-CN: 活跃分片数 + en-US: Active Shards + ja-JP: 活動中シャーズ数 + - field: active_percentage + type: 0 + unit: '%' + i18n: + zh-CN: 分片健康度(%) + en-US: Active Percentage + ja-JP: シャーズの健全率 + - field: initializing_shards + type: 0 + i18n: + zh-CN: 初始化分片数 + en-US: Initializing Shards + ja-JP: 初期化シャーズ数 + - field: unassigned_shards + type: 0 + i18n: + zh-CN: 未分配分片数 + en-US: Unassigned Shards + ja-JP: 未割り当てシャーズ数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - cluster_name + - status + - number_of_nodes + - number_of_data_nodes + - active_primary_shards + - active_shards + - initializing_shards + - unassigned_shards + - active_shards_percent_as_number + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - cluster_name=cluster_name + - status=status + - nodes=number_of_nodes + - data_nodes=number_of_data_nodes + - active_primary_shards=active_primary_shards + - active_shards=active_shards + - active_percentage=active_shards_percent_as_number + - initializing_shards=initializing_shards + - unassigned_shards=unassigned_shards + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /_cluster/health?pretty + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: default + + - name: nodes + i18n: + zh-CN: 集群节点信息 + en-US: Cluster Nodes + ja-JP: クラスタノード情報 + priority: 1 + fields: + - field: total + type: 0 + i18n: + zh-CN: 节点数 + en-US: total + ja-JP: ノード数 + - field: successful + type: 0 + i18n: + zh-CN: 在线节点数 + en-US: successful + ja-JP: オンラインノード数 + - field: failed + type: 0 + i18n: + zh-CN: 离线节点数 + en-US: failed + ja-JP: オフラインノード数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /_nodes/stats + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + # parse script when parse type is josnPath + parseScript: '$._nodes' + + - name: nodes_detail + i18n: + zh-CN: 节点详细信息 + en-US: Node Detail + ja-JP: ノード情報 + priority: 2 + fields: + - field: node_name + type: 1 + label: true + i18n: + zh-CN: 节点名称 + en-US: Node Name + ja-JP: ノード名 + - field: ip + type: 1 + i18n: + zh-CN: IP地址 + en-US: IP Address + ja-JP: IP アドレス + - field: cpu_load_average + type: 0 + i18n: + zh-CN: CPU平均负载 + en-US: Cpu Load Average + ja-JP: CPUロードアベレージ + - field: cpu_percent + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: Cpu Percent + ja-JP: CPU使用率 + - field: heap_used + type: 0 + unit: MB + i18n: + zh-CN: 内存使用量(MB) + en-US: Heap Used + ja-JP: メモリ使用量(MB) + - field: heap_used_percent + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Heap Used Percent + ja-JP: メモリ使用率 + - field: heap_total + type: 1 + unit: 'MB' + i18n: + zh-CN: 总内存(MB) + en-US: Heap Total + ja-JP: メモリ容量(MB) + - field: disk_free + type: 0 + unit: 'GB' + i18n: + zh-CN: 磁盘剩余容量(GB) + en-US: Disk Free + ja-JP: 利用可能なディスク容量 + - field: disk_total + type: 1 + unit: 'GB' + i18n: + zh-CN: 磁盘总容量 + en-US: Disk Total + ja-JP: ディスク容量 + - field: disk_used_percent + type: 0 + unit: '%' + i18n: + zh-CN: 磁盘使用率 + en-US: Disk Used Percent + ja-JP: ディスク使用率 + aliasFields: + - $.name + - $.ip + - $.os.cpu.load_average.1m + - $.os.load_average + - $.os.cpu_percent + - $.os.cpu.percent + - $.jvm.mem.heap_used_in_bytes + - $.jvm.mem.heap_used_percent + - $.jvm.mem.heap_max_in_bytes + - $.fs.total.free_in_bytes + - $.fs.total.total_in_bytes + calculates: + - node_name=$.name + - ip=$.ip + - cpu_load_average=$.os.cpu.load_average.1m!=null?$.os.cpu.load_average.1m:$.os.load_average + - cpu_percent=$.os.cpu.percent!=null?$.os.cpu.percent:$.os.cpu_percent + - heap_used=$.jvm.mem.heap_used_in_bytes + - heap_used_percent=$.jvm.mem.heap_used_percent + - heap_total=$.jvm.mem.heap_max_in_bytes + - disk_free=$.fs.total.free_in_bytes + - disk_total=$.fs.total.total_in_bytes + - disk_used_percent=(1-$.fs.total.free_in_bytes/$.fs.total.total_in_bytes)*100 + units: + - heap_used=B->MB + - heap_total=B->MB + - disk_free=B->GB + - disk_total=B->GB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /_nodes/stats + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.nodes.*' diff --git a/hertzbeat-manager/src/main/resources/define/app-emqx.yml b/hertzbeat-manager/src/main/resources/define/app-emqx.yml new file mode 100644 index 0000000..e6d5d80 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-emqx.yml @@ -0,0 +1,535 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: mid +# The monitoring type eg: linux windows tomcat mysql aws... +app: emqx +# The app api i18n name +name: + zh-CN: EMQX MQTT + en-US: EMQX MQTT + ja-JP: EMQX MQTT +# The description and help of this monitoring type +help: + zh-CN: EMQX 是一款开源的大规模分布式 MQTT 消息服务器。Hertzbeat对EMQX MQTT消息服务器 5.0+ 版本的通用指标进行测量监控,<br>您可以点击 “<i>新建 EMQX 消息服务器</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: EMQX is an open source large-scale distributed MQTT message server. Hertzbeat measures and monitors common metrics of the EMQX message server 5.0+ version.<br>You can click "<i>New EMQX message server</i>" and configure it, or select "<i>More operations</i>", Import existing configuration. + zh-TW: EMQX 是一款開源的大規模分散式 MQTT 訊息伺服器。 Hertzbeat對EMQX MQTT訊息伺服器 5.0+ 版本的通用指標進行測量監控,<br>您可以點擊“<i>新建 EMQX 訊息伺服器</i>” 並進行配置,或者選擇“<i>更多操作</i>” ,導入已有配置。 + ja-JP: EMQXは、オープンソースの大規模分散MQTTメッセージサーバーです。HertzbeatはEMQX MQTTメッセージサーバー(バージョン5.0+)の一般的なフォーマンスのメトリクスを監視します。<br>「<i>新規 EMQX</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/emqx + en-US: https://hertzbeat.apache.org/docs/help/emqx +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 18083 + - field: ssl + name: + zh-CN: 启用HTTPS + en-US: HTTPS + ja-JP: HTTPS + type: boolean + defaultValue: false + - field: timeout + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + type: number + required: false + hide: true + - field: authType + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + type: radio + required: true + options: + - label: Basic Auth + value: Basic Auth + defaultValue: Basic Auth + - field: apikey + name: + zh-CN: API Key + en-US: API Key + ja-JP: API キー + type: text + required: true + - field: secretkey + name: + zh-CN: Secret Key + en-US: Secret Key + ja-JP: 鍵 + type: text + required: true +metrics: + # metrics - summary + - name: summary + # metrics name i18n label + i18n: + zh-CN: 概要 + en-US: Summary + ja-JP: 概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # metrics content contains field-metric name, type-metric type:0-number,1-string, instance-if is metrics, unit-metric unit('%','ms','MB') + - field: version + type: 1 + i18n: + zh-CN: 系统版本 + en-US: Version + ja-JP: バージョン + - field: node_name + type: 1 + i18n: + zh-CN: 节点名称 + en-US: Node Name + ja-JP: ノード名 + - field: broker_status + type: 1 + i18n: + zh-CN: Broker 状态 + en-US: Broker Status + ja-JP: ブローカーステータス + - field: app_status + type: 1 + i18n: + zh-CN: 应用状态 + en-US: App Status + ja-JP: Appステータス + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - rel_vsn + - node_name + - broker_status + - app_status + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - version=rel_vsn + - node_name=node_name + - broker_status=broker_status + - app_status=app_status + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, we use HTTP protocol here + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url, we don't need to enter a parameter here, just set the fixed value to /api/summary + url: /api/v5/status?format=json + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH, default fixed value is GET + method: GET + # if enabled https, default value is false + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^apikey^_^ + basicAuthPassword: ^_^secretkey^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, we use jsonpath to parse response data here + parseType: jsonPath + parseScript: '$' + + - name: metrics + i18n: + zh-CN: 指标 + en-US: Metrics + ja-JP: メトリクス + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # metrics content contains field-metric name, type-metric type:0-number,1-string, instance-if is metrics, unit-metric unit('%','ms','MB') + - field: client_connected + type: 0 + i18n: + zh-CN: 客户端连接 + en-US: Client Connected + ja-JP: クライアント接続済み + - field: client_disconnected + type: 0 + i18n: + zh-CN: 客户端断开 + en-US: Client Disconnected + ja-JP: クライアント切断 + - field: packets_sent + type: 0 + i18n: + zh-CN: 发送数据包 + en-US: Packets Sent + ja-JP: 転送されたパケット + - field: packets_received + type: 0 + i18n: + zh-CN: 接收数据包 + en-US: Packets Received + ja-JP: 受信されたパケット + - field: bytes_sent + type: 0 + i18n: + zh-CN: 发送字节 + en-US: Bytes Sent + ja-JP: 転送されたバイト数 + - field: bytes_received + type: 0 + i18n: + zh-CN: 接收字节 + en-US: Bytes Received + ja-JP: 受信されたバイト数 + - field: messages_sent + type: 0 + i18n: + zh-CN: 发送消息 + en-US: Messages Sent + ja-JP: 転送されたメッセージ + - field: messages_acked + type: 0 + i18n: + zh-CN: 消息确认 + en-US: Messages Acked + ja-JP: 確認メッセージ + - field: messages_delayed + type: 0 + i18n: + zh-CN: 延迟消息 + en-US: Messages Delayed + ja-JP: 遅延メッセージ + - field: authorization_deny + type: 0 + i18n: + zh-CN: 授权拒绝 + en-US: Authorization Deny + ja-JP: 認証拒否 + - field: client_authorize + type: 0 + i18n: + zh-CN: 客户端授权 + en-US: Client Authorize + ja-JP: クライアント認証 + - field: session_created + type: 0 + i18n: + zh-CN: 会话创建 + en-US: Session Created + ja-JP: 作成されたセッション + - field: session_discarded + type: 0 + i18n: + zh-CN: 会话丢弃 + en-US: Session Discarded + ja-JP: 廃棄されたセッション + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - client.connected + - client.disconnected + - packets.sent + - packets.received + - bytes.sent + - bytes.received + - messages.sent + - messages.acked + - messages.delayed + - authorization.deny + - client.authorize + - session.created + - session.discarded + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - client_connected=client.connected + - client_disconnected=client.disconnected + - packets_sent=packets.sent + - packets_received=packets.received + - bytes_sent=bytes.sent + - bytes_received=bytes.received + - messages_sent=messages.sent + - messages_acked=messages.acked + - messages_delayed=messages.delayed + - authorization_deny=authorization.deny + - client_authorize=client.authorize + - session_created=session.created + - session_discarded=session.discarded + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, we use HTTP protocol here + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url, we don't need to enter a parameter here, just set the fixed value to /api/summary + url: /api/v5/metrics?aggregate=true + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH, default fixed value is GET + method: GET + # if enabled https, default value is false + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^apikey^_^ + basicAuthPassword: ^_^secretkey^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, we use jsonpath to parse response data here + parseType: jsonPath + parseScript: '$' + + - name: stats + i18n: + zh-CN: 统计 + en-US: Stats + ja-JP: 統計 + priority: 2 + fields: + # metrics content contains field-metric name, type-metric type:0-number,1-string, instance-if is metrics, unit-metric unit('%','ms','MB') + - field: channels_count + type: 0 + i18n: + zh-CN: 通道数 + en-US: Channels Count + ja-JP: チャンネル数 + - field: channels_max + type: 0 + i18n: + zh-CN: 通道最大数 + en-US: Channels Max + ja-JP: 最大チャンネル数 + - field: connections_count + type: 0 + i18n: + zh-CN: 连接数 + en-US: Connections Count + ja-JP: 接続数 + - field: connections_max + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Connections Max + ja-JP: 最大接続数 + - field: delayed_count + type: 0 + i18n: + zh-CN: 延迟数 + en-US: Delayed Count + ja-JP: 遅延数 + - field: delayed_max + type: 0 + i18n: + zh-CN: 最大延迟数 + en-US: Delayed Max + ja-JP: 最大遅延数 + - field: live_connections_count + type: 0 + i18n: + zh-CN: 活动连接数 + en-US: Live Connections Count + ja-JP: ライブ接続数 + - field: live_connections_max + type: 0 + i18n: + zh-CN: 最大活动连接数 + en-US: Live Connections Max + ja-JP: 最大ライブ接続数 + - field: retained_count + type: 0 + i18n: + zh-CN: 保留数 + en-US: Retained Count + ja-JP: 保持数 + - field: retained_max + type: 0 + i18n: + zh-CN: 最大保留数 + en-US: Retained Max + ja-JP: 最大保持数 + - field: sessions_count + type: 0 + i18n: + zh-CN: 会话数 + en-US: Sessions Count + ja-JP: セッション数 + - field: sessions_max + type: 0 + i18n: + zh-CN: 最大会话数 + en-US: Sessions Max + ja-JP: 最大セッション数 + - field: suboptions_count + type: 0 + i18n: + zh-CN: 订阅选项数 + en-US: Suboptions Count + ja-JP: サブスクリプションのオプション数 + - field: suboptions_max + type: 0 + i18n: + zh-CN: 最大订阅选项数 + en-US: Suboptions Max + ja-JP: 最大サブスクリプションのオプション数 + - field: subscribers_count + type: 0 + i18n: + zh-CN: 订阅者数 + en-US: Subscribers Count + ja-JP: サブスクライバー数 + - field: subscribers_max + type: 0 + i18n: + zh-CN: 最大订阅者数 + en-US: Subscribers Max + ja-JP: 最大サブスクライバー数 + - field: subscriptions_count + type: 0 + i18n: + zh-CN: 订阅数 + en-US: Subscriptions Count + ja-JP: サブスクリプション数 + - field: subscriptions_max + type: 0 + i18n: + zh-CN: 最大订阅数 + en-US: Subscriptions Max + ja-JP: 最大サブスクリプション数 + - field: subscriptions_shared_count + type: 0 + i18n: + zh-CN: 共享订阅数 + en-US: Subscriptions Shared Count + ja-JP: 共有サブスクリプション数 + - field: subscriptions_shared_max + type: 0 + i18n: + zh-CN: 最大共享订阅数 + en-US: Subscriptions Shared Max + ja-JP: 最大共有サブスクリプション数 + - field: topics_count + type: 0 + i18n: + zh-CN: 主题数 + en-US: Topics Count + ja-JP: トピック数 + - field: topics_max + type: 0 + i18n: + zh-CN: 最大主题数 + en-US: Topics Max + ja-JP: 最大トピック数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - channels.count + - channels.max + - connections.count + - connections.max + - delayed.count + - delayed.max + - live_connections.count + - live_connections.max + - retained.count + - retained.max + - sessions.count + - sessions.max + - suboptions.count + - suboptions.max + - subscribers.count + - subscribers.max + - subscriptions.count + - subscriptions.max + - subscriptions.shared.count + - subscriptions.shared.max + - topics.count + - topics.max + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - channels_count=channels.count + - channels_max=channels.max + - connections_count=connections.count + - connections_max=connections.max + - delayed_count=delayed.count + - delayed_max=delayed.max + - live_connections_count=live_connections.count + - live_connections_max=live_connections.max + - retained_count=retained.count + - retained_max=retained.max + - sessions_count=sessions.count + - sessions_max=sessions.max + - suboptions_count=suboptions.count + - suboptions_max=suboptions.max + - subscribers_count=subscribers.count + - subscribers_max=subscribers.max + - subscriptions_count=subscriptions.count + - subscriptions_max=subscriptions.max + - subscriptions_shared_count=subscriptions.shared.count + - subscriptions_shared_max=subscriptions.shared.max + - topics_count=topics.count + - topics_max=topics.max + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, we use HTTP protocol here + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url, we don't need to enter a parameter here, just set the fixed value to /api/summary + url: /api/v5/stats?aggregate=true + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH, default fixed value is GET + method: GET + # if enabled https, default value is false + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^apikey^_^ + basicAuthPassword: ^_^secretkey^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, we use jsonpath to parse response data here + parseType: jsonPath + parseScript: '$' diff --git a/hertzbeat-manager/src/main/resources/define/app-euleros.yml b/hertzbeat-manager/src/main/resources/define/app-euleros.yml new file mode 100644 index 0000000..2488706 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-euleros.yml @@ -0,0 +1,765 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: euleros +# The monitoring i18n name +name: + zh-CN: EulerOS 操作系统 + en-US: EulerOS + ja-JP: EulerOS +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 EulerOS 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 EulerOS</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors EulerOS operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New EulerOS</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 EulerOS 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 EulerOS</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> EulerOS システムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 EulerOS</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/euleros + en-US: https://hertzbeat.apache.org/docs/help/euleros +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: (uname -r ; hostname ; uptime | awk -F "," '{print $1}' | sed "s/ //g") | sed ":a;N;s/\n/^/g;ta" | awk -F '^' 'BEGIN{print "version hostname uptime"} {print $1, $2, $3}' + # ssh response data parse type: oneRow, multiRow + parseType: multiRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100-idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: "LANG=C lscpu | awk -F: '$1==\"Model name\" {print $2}';awk '/processor/{core++} END{print core}' /proc/cpuinfo;uptime | sed 's/,/ /g' | awk '{for(i=NF-2;i<=NF;i++)print $i }' | xargs;vmstat 1 1 | awk 'NR==3{print $11}';vmstat 1 1 | awk 'NR==3{print $12}';vmstat 1 2 | awk 'NR==4{print $15}'" + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: free -m | awk 'BEGIN{print "total used free buff_cache available"} NR==2{print $2,$3,$4,$6,$7}' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + en-US: Partition Num + ja-JP: パーティション + - field: block_write + type: 0 + i18n: + zh-CN: 写磁盘块数 + en-US: Block Write + ja-JP: 書き込みディスクブロック数 + - field: block_read + type: 0 + i18n: + zh-CN: 读磁盘块数 + en-US: Block Read + ja-JP: 読み取りブロック数 + - field: write_rate + type: 0 + unit: iops + i18n: + zh-CN: 磁盘写速率 + en-US: Write Rate + ja-JP: ディスク書き込み速度 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: vmstat -D | awk 'NR==1{print $1}';vmstat -D | awk 'NR==2{print $1}';vmstat 1 1 | awk 'NR==3{print $10}';vmstat 1 1 | awk 'NR==3{print $9}';vmstat 1 1 | awk 'NR==3{print $16}' + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: cat /proc/net/dev | tail -n +3 | awk 'BEGIN{ print "interface_name receive_bytes transmit_bytes"} {print $1,$2,$10}' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -mP | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-eureka_sd.yml b/hertzbeat-manager/src/main/resources/define/app-eureka_sd.yml new file mode 100644 index 0000000..f34db06 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-eureka_sd.yml @@ -0,0 +1,67 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: __system__ +# Monitoring application type name (consistent with file name) eg: linux windows tomcat mysql aws... +app: eureka_sd +# The app api i18n name +name: + zh-CN: Eureka Service Discovery + en-US: Eureka Service Discovery + ja-JP: Eureka サービスディスカバリー +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: __sd_url__ + # name-param field display i18n name + name: + zh-CN: Eureka服务发现地址 + en-US: Eureka Service Discovery Url + ja-JP: Eureka サービスディスカバリーURL + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true + +metrics: + - name: target + i18n: + zh-CN: 监控目标 + en-US: Monitor Target + ja-JP: 監視対象 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: host + type: 1 + i18n: + zh-CN: Host + en-US: Host + ja-JP: ホスト + - field: port + type: 1 + i18n: + zh-CN: Port + en-US: Port + ja-JP: ポート + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: eureka_sd + # the config content when protocol is http_sd + eureka_sd: + url: ^_^__sd_url__^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-flink.yml b/hertzbeat-manager/src/main/resources/define/app-flink.yml new file mode 100644 index 0000000..a6ba226 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-flink.yml @@ -0,0 +1,206 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: flink +# The monitoring i18n name +name: + zh-CN: Apache Flink + en-US: Apache Flink + ja-JP: Apache Flink +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Flink流引擎的通用指标进行测量监控。<br>您可以点击 “<i>新建 Flink流引擎</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitoring Flink Stream through general performance metric. You could click the "<i>New Flink Stream</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 Flink流引擎的通用名額進行量測監控。<br>您可以點擊“<i>新建Flink流引擎</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: HertzBeat は Flinkの一般的なメトリクスを監視します。<br>「<i>新規 Flink</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/flink + en-US: https://hertzbeat.apache.org/docs/help/flink +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 8081 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL利用 + # type-param field type(boolean mapping the html switch tag) + type: boolean + # required-true or false + required: false + # field-param field key + - field: authType + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + # type-param field type(most mapping the html input type) + type: radio + required: false + hide: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + hide: true + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + hide: true +# collect metrics config list +metrics: + # metrics - basic + - name: basic + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: flink-version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /v1/config + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: default + + - name: overview + priority: 1 + fields: + - field: slots_total # slots count + type: 0 + i18n: + zh-CN: 插槽总数 + en-US: Slots Total + ja-JP: スロット数 + - field: slots_used # slots used count + type: 0 + i18n: + zh-CN: 已用插槽数 + en-US: Slots Used + ja-JP: 使用したスロット数 + - field: task_total # task count + type: 0 + i18n: + zh-CN: 任务总数 + en-US: Task Total + ja-JP: タスク数 + - field: jobs_running # Number of running tasks + type: 0 + i18n: + zh-CN: 正在运行的任务数 + en-US: Jobs Running + ja-JP: 実行中のタスク数 + - field: jobs_failed # Number of failed tasks + type: 0 + i18n: + zh-CN: 已经失败的任务数 + en-US: Jobs Failed + ja-JP: 失敗したタスク数 + aliasFields: + - slots-total + - slots-available + - jobs-running + - jobs-finished + - jobs-cancelled + - jobs-failed + calculates: + - slots_total=get('slots-total') + - slots_used=get('slots-total')-get('slots-available') + - task_total=get('jobs-running')+get('jobs-finished')+get('jobs-cancelled')+get('jobs-failed') + - jobs_running=get('jobs-running') + - jobs_failed=get('jobs-failed') + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /v1/overview + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: default diff --git a/hertzbeat-manager/src/main/resources/define/app-flink_on_yarn.yml b/hertzbeat-manager/src/main/resources/define/app-flink_on_yarn.yml new file mode 100644 index 0000000..556ee5f --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-flink_on_yarn.yml @@ -0,0 +1,530 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: flink_on_yarn +# The monitoring i18n name +name: + zh-CN: Apache Flink On Yarn + en-US: Apache Flink On Yarn + ja-JP: Apache Flink On Yarn +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Flink 流引擎 Yarn 模式的通用指标进行测量监控。<br>您可以点击 “<i>新建 Flink On Yarn</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitoring Flink Stream through general performance metric. You could click the "<i>New Flink Stream</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 Flink 流引擎 Yarn 模式的通用指標進行測量監控。<br>您可以點擊 “<i>新建 Flink On Yarn</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: HertzBeat は Flink 「Yarn モード」の一般的なメトリクスを監視します。<br>「<i>新規 Flink</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/flink_on_yarn + en-US: https://hertzbeat.apache.org/docs/help/flink_on_yarn +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: Yarn端口 + en-US: Yarn Port + ja-JP: Yarnポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 8088 + # field-param field key + - field: application_id + # name-param field display i18n name + name: + zh-CN: 应用ID + en-US: Application ID + ja-JP: 応用ID + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL利用 + # type-param field type(boolean mapping the html switch tag) + type: boolean + # required-true or false + required: false + # field-param field key + - field: authType + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + # type-param field type(most mapping the html input type) + type: radio + required: false + hide: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + hide: true + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + hide: true +# collect metrics config list +metrics: + - name: jobmanager + i18n: + zh-CN: JobManager Metrics + en-US: JobManager Metrics + ja-JP: JobManagerメトリクス + priority: 0 + fields: + - field: id + type: 1 + label: true + i18n: + zh-CN: 监控项 + en-US: key + ja-JP: キー + - field: value + type: 0 + i18n: + zh-CN: 值 + en-US: value + ja-JP: 値 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /proxy/^_^application_id^_^/jobmanager/metrics?get=Status.JVM.Memory.Heap.Used,Status.JVM.Memory.Heap.Max,Status.JVM.Memory.Metaspace.Used,Status.JVM.Memory.Metaspace.Max,Status.JVM.Memory.Heap.Committed,Status.JVM.Memory.Heap.Used,Status.JVM.Memory.Heap.Max,Status.JVM.Memory.NonHeap.Committed,Status.JVM.Memory.NonHeap.Used,Status.JVM.Memory.NonHeap.Max,Status.JVM.Memory.Direct.Count,Status.JVM.Memory.Direct.MemoryUsed,Status.JVM.Memory.Direct.TotalCapacity,Status.JVM.Memory.Mapped.Count,Status.JVM.Memory.Mapped.MemoryUsed,Status.JVM.Memory.Mapped.TotalCapacity,Status.JVM.GarbageCollector.G1_Young_Generation.Count,Status.JVM.GarbageCollector.G1_Old_Generation.Count,Status.JVM.GarbageCollector.G1_Old_Generation.Time,Status.JVM.GarbageCollector.G1_Young_Generation.Time + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.*' + - name: jobmanager_config + i18n: + zh-CN: JobManager Config + en-US: JobManager Config + ja-JP: JobManager設定 + priority: 1 + fields: + - field: key + type: 1 + i18n: + zh-CN: 监控项 + en-US: key + ja-JP: キー + - field: value + type: 1 + i18n: + zh-CN: 值 + en-US: value + ja-JP: 値 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /proxy/^_^application_id^_^/jobmanager/config + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.*' + + - name: taskmanager + i18n: + zh-CN: TaskManager + en-US: TaskManager + ja-JP: TaskManager + priority: 2 + fields: + - field: id + type: 1 + label: true + i18n: + zh-CN: Container ID + en-US: Container ID + ja-JP: コンテナID + - field: path + type: 1 + i18n: + zh-CN: Path + en-US: Path + ja-JP: パス + - field: dataPort + type: 1 + i18n: + zh-CN: Data Port + en-US: Data Port + ja-JP: データポート + - field: jmxPort + type: 1 + i18n: + zh-CN: JMX Port + en-US: JMX Port + ja-JP: JMXポート + - field: timeSinceLastHeartbeat + type: 0 + i18n: + zh-CN: Last Heartbeat + en-US: Last Heartbeat + ja-JP: 最後のハートビート + - field: slotsNumber + type: 0 + i18n: + zh-CN: All Slots + en-US: All Slots + ja-JP: スロット + - field: freeSlots + type: 0 + i18n: + zh-CN: Free Slots + en-US: Free Slots + ja-JP: 利用可能なスロット + - field: totalResourceCpuCores + type: 0 + i18n: + zh-CN: totalResourceCpuCores + en-US: totalResourceCpuCores + ja-JP: totalResourceCpuCores + - field: totalResourceTaskHeapMemory + type: 0 + i18n: + zh-CN: totalResourceTaskHeapMemory + en-US: totalResourceTaskHeapMemory + ja-JP: totalResourceTaskHeapMemory + unit: MB + - field: totalResourceManagedMemory + type: 0 + i18n: + zh-CN: totalResourceManagedMemory + en-US: totalResourceManagedMemory + ja-JP: totalResourceManagedMemory + unit: MB + - field: totalResourceNetworkMemory + type: 0 + i18n: + zh-CN: totalResourceNetworkMemory + en-US: totalResourceNetworkMemory + ja-JP: totalResourceNetworkMemory + unit: MB + - field: freeResourceCpuCores + type: 0 + i18n: + zh-CN: freeResourceCpuCores + en-US: freeResourceCpuCores + ja-JP: freeResourceCpuCores + - field: freeResourceTaskHeapMemory + type: 0 + i18n: + zh-CN: freeResourceTaskHeapMemory + en-US: freeResourceTaskHeapMemory + ja-JP: freeResourceTaskHeapMemory + unit: MB + - field: freeResourceTaskOffHeapMemory + type: 0 + i18n: + zh-CN: freeResourceTaskOffHeapMemory + en-US: freeResourceTaskOffHeapMemory + ja-JP: freeResourceTaskOffHeapMemory + unit: MB + - field: freeResourceManagedMemory + type: 0 + i18n: + zh-CN: freeResourceManagedMemory + en-US: freeResourceManagedMemory + ja-JP: freeResourceManagedMemory + unit: MB + - field: freeResourceNetworkMemory + type: 0 + i18n: + zh-CN: freeResourceNetworkMemory + en-US: freeResourceNetworkMemory + ja-JP: freeResourceNetworkMemory + unit: MB + - field: hardwareCpuCores + type: 0 + i18n: + zh-CN: CPU Cores + en-US: CPU Cores + ja-JP: Cpuコア数 + - field: hardwarePhysicalMemory + type: 0 + i18n: + zh-CN: Physical MEM + en-US: Physical MEM + ja-JP: 物理メモリ + unit: GB + - field: hardwareFreeMemory + type: 0 + i18n: + zh-CN: JVM Heap Size + en-US: JVM Heap Size + ja-JP: Java仮想マシンのヒープサイズ + unit: MB + - field: hardwareManagedMemory + type: 0 + i18n: + zh-CN: Flink Managed MEM + en-US: Flink Managed MEM + ja-JP: Flink管理メモリ + unit: MB + - field: memoryConfigurationFrameworkHeap + type: 0 + i18n: + zh-CN: Framework Heap + en-US: Framework Heap + ja-JP: フレームワークのヒープ + unit: MB + - field: memoryConfigurationTaskHeap + type: 0 + i18n: + zh-CN: Task Heap + en-US: Task Heap + ja-JP: タスクヒープ + unit: MB + - field: memoryConfigurationFrameworkOffHeap + type: 0 + i18n: + zh-CN: Framework Off-Heap + en-US: Framework Off-Heap + ja-JP: フレームワークのオフヒープ + unit: MB + - field: memoryConfigurationTaskOffHeap + type: 0 + i18n: + zh-CN: Task Off-Heap + en-US: Task Off-Heap + ja-JP: タスクオフヒープ + unit: MB + - field: memoryConfigurationNetworkMemory + type: 0 + i18n: + zh-CN: Network + en-US: Network + ja-JP: ネットワーク + unit: MB + - field: memoryConfigurationManagedMemory + type: 0 + i18n: + zh-CN: Managed Memory + en-US: Managed Memory + ja-JP: 管理メモリ + unit: MB + - field: memoryConfigurationJvmMetaspace + type: 0 + i18n: + zh-CN: JVM Metaspace + en-US: JVM Metaspace + ja-JP: Java仮想マシンのメタスペース + unit: MB + - field: memoryConfigurationJvmOverhead + type: 0 + i18n: + zh-CN: JVM Overhead + en-US: JVM Overhead + ja-JP: Java仮想マシンのオーバーヘッド + - field: memoryConfigurationTotalFlinkMemory + type: 0 + i18n: + zh-CN: memoryConfigurationTotalFlinkMemory + en-US: memoryConfigurationTotalFlinkMemory + ja-JP: memoryConfigurationTotalFlinkMemory + - field: memoryConfigurationTotalProcessMemory + type: 0 + i18n: + zh-CN: memoryConfigurationTotalProcessMemory + en-US: memoryConfigurationTotalProcessMemory + ja-JP: memoryConfigurationTotalProcessMemory + aliasFields: + - $.id + - $.path + - $.dataPort + - $.jmxPort + - $.timeSinceLastHeartbeat + - $.slotsNumber + - $.freeSlots + - $.totalResource.cpuCores + - $.totalResource.taskHeapMemory + - $.totalResource.managedMemory + - $.totalResource.networkMemory + - $.freeResource.cpuCores + - $.freeResource.taskHeapMemory + - $.freeResource.taskOffHeapMemory + - $.freeResource.managedMemory + - $.freeResource.networkMemory + - $.hardware.cpuCores + - $.hardware.physicalMemory + - $.hardware.freeMemory + - $.hardware.managedMemory + - $.memoryConfiguration.frameworkHeap + - $.memoryConfiguration.taskHeap + - $.memoryConfiguration.frameworkOffHeap + - $.memoryConfiguration.taskOffHeap + - $.memoryConfiguration.networkMemory + - $.memoryConfiguration.managedMemory + - $.memoryConfiguration.jvmMetaspace + - $.memoryConfiguration.jvmOverhead + - $.memoryConfiguration.totalFlinkMemory + - $.memoryConfiguration.totalProcessMemory + calculates: + - id=$.id + - path=$.path + - dataPort=$.dataPort + - jmxPort=$.jmxPort + - timeSinceLastHeartbeat=$.timeSinceLastHeartbeat + - slotsNumber=$.slotsNumber + - freeSlots=$.freeSlots + - totalResourceCpuCores=$.totalResource.cpuCores + - totalResourceTaskHeapMemory=$.totalResource.taskHeapMemory + - totalResourceManagedMemory=$.totalResource.managedMemory + - totalResourceNetworkMemory=$.totalResource.networkMemory + - freeResourceCpuCores=$.freeResource.cpuCores + - freeResourceTaskHeapMemory=$.freeResource.taskHeapMemory + - freeResourceTaskOffHeapMemory=$.freeResource.taskOffHeapMemory + - freeResourceManagedMemory=$.freeResource.managedMemory + - freeResourceNetworkMemory=$.freeResource.networkMemory + - hardwareCpuCores=$.hardware.cpuCores + - hardwarePhysicalMemory=$.hardware.physicalMemory + - hardwareFreeMemory=$.hardware.freeMemory + - hardwareManagedMemory=$.hardware.managedMemory + - memoryConfigurationFrameworkHeap=$.memoryConfiguration.frameworkHeap + - memoryConfigurationTaskHeap=$.memoryConfiguration.taskHeap + - memoryConfigurationFrameworkOffHeap=$.memoryConfiguration.frameworkOffHeap + - memoryConfigurationTaskOffHeap=$.memoryConfiguration.taskOffHeap + - memoryConfigurationNetworkMemory=$.memoryConfiguration.networkMemory + - memoryConfigurationManagedMemory=$.memoryConfiguration.managedMemory + - memoryConfigurationJvmMetaspace=$.memoryConfiguration.jvmMetaspace + - memoryConfigurationJvmOverhead=$.memoryConfiguration.jvmOverhead + - memoryConfigurationTotalFlinkMemory=$.memoryConfiguration.totalFlinkMemory + - memoryConfigurationTotalProcessMemory=$.memoryConfiguration.totalProcessMemory + units: + - hardwarePhysicalMemory = B->GB + - hardwareFreeMemory = B->MB + - hardwareManagedMemory = B->MB + - memoryConfigurationFrameworkHeap = B->MB + - memoryConfigurationTaskHeap = B->MB + - memoryConfigurationManagedMemory = B->MB + - memoryConfigurationFrameworkOffHeap = B->MB + - memoryConfigurationTaskOffHeap = B->MB + - memoryConfigurationNetworkMemory = B->MB + - memoryConfigurationJvmMetaspace = B->MB + - memoryConfigurationJvmOverhead = B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /proxy/^_^application_id^_^/taskmanagers + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.taskmanagers.*' + + - name: taskmanager_metrics + i18n: + zh-CN: TaskManager Metrics + en-US: TaskManager Metrics + ja-JP: TaskManagerメトリクス + priority: 3 + fields: + - field: container_id + i18n: + zh-CN: Container ID + en-US: Container ID + ja-JP: コンテナID + type: 1 + label: true + - field: id + type: 1 + i18n: + zh-CN: 监控项 + en-US: key + ja-JP: キー + label: true + - field: value + type: 0 + i18n: + zh-CN: 值 + en-US: value + ja-JP: 値 + unit: MB + units: + - value=B->MB + calculates: + - container_id="^o^id^o^" + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /proxy/^_^application_id^_^/taskmanagers/^o^id^o^/metrics?get=Status.JVM.Memory.Heap.Used,Status.JVM.Memory.Heap.Max,Status.Shuffle.Netty.UsedMemory,Status.Shuffle.Netty.TotalMemory,Status.Flink.Memory.Managed.Used,Status.Flink.Memory.Managed.Total,Status.JVM.Memory.Metaspace.Used,Status.JVM.Memory.Metaspace.Max + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.*' diff --git a/hertzbeat-manager/src/main/resources/define/app-freebsd.yml b/hertzbeat-manager/src/main/resources/define/app-freebsd.yml new file mode 100644 index 0000000..46aed45 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-freebsd.yml @@ -0,0 +1,643 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: freebsd +# The monitoring i18n name +name: + zh-CN: FreeBSD操作系统 + en-US: OS FreeBSD + ja-JP: OS FreeBSD +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 FreeBSD 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 FreeBSD</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors FreeBSD operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New FreeBSD</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 FreeBSD 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 FreeBSD</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> Centosシステムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 FreeBSD</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn//docs/help/freebsd + en-US: https://hertzbeat.apache.org/docs/help/freebsd +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: uname -r ; hostname ; uptime | awk -F "," '{print $1}' | sed "s/ //g" + # ssh response data parse type: oneRow, multiRow + parseType: oneRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 0 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + # (可选)监控指标别名, 做为中间字段与采集数据字段和指标字段映射转换 + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100-idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: "dmesg | grep -i cpu | awk 'NR==1';sysctl hw.ncpu | awk '{print $2}';uptime | sed 's/,/ /g' | awk '{for(i=NF-2;i<=NF;i++)print $i }' | xargs;vmstat 1 1 | awk 'NR==3{print $11}';vmstat 1 1 | awk 'NR==3{print $12}';vmstat 1 2 | awk 'NR==4{print $15}'" + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: physmem + type: 0 + unit: Mb + i18n: + zh-CN: 物理内存 + en-US: Physics Memory + ja-JP: 物理メモリ + - field: usermem + type: 0 + unit: Mb + i18n: + zh-CN: 用户内存 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: realmem + type: 0 + unit: Mb + i18n: + zh-CN: 实际内存 + en-US: Real Memory + ja-JP: 実際のメモリ + - field: availmem + type: 0 + unit: Mb + i18n: + zh-CN: 可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - physmem + - usermem + - realmem + - availmem + calculates: + - physmem=physmem + - usermem=usermem + - realmem=realmem + - availmem=availmem + - usage=((realmem - availmem) / realmem) * 100 + units: + - physmem=B->MB + - usermem=B->MB + - realmem=B->MB + - availmem=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: sysctl hw | egrep 'hw.(phys|user|real)' | awk '{print $2}';dmesg | grep memory | grep avail | awk '{print $4}'; + parseType: oneRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ディスク情報 + priority: 3 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 利用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -m | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 4 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 5 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-ftp.yml b/hertzbeat-manager/src/main/resources/define/app-ftp.yml new file mode 100644 index 0000000..1fabc0a --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-ftp.yml @@ -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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: ftp +# The monitoring i18n name +name: + zh-CN: FTP服务器 + en-US: FTP Server + ja-JP: FTPサーバー +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 FTP 服务器的通用指标进行测量监控。<br>您可以点击 “<i>新建 FTP服务器</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitoring FTP server through general performance metric. You could click the "<i>New FTP server</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 FTP 伺服器的通用名額進行量測監控。<br>您可以點擊“<i>新建FTP伺服器</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は FTPサーバーの一般的なメトリクスを監視します。<br>「<i>新規 FTPサーバー</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/ftp + en-US: https://hertzbeat.apache.org/docs/help/ftp +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 21 + # param field input placeholder + placeholder: 'Please input port' + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + hide: true + - field: password + name: + zh-CN: 用户密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + required: false + hide: true + - field: direction + name: + zh-CN: 目录 + en-US: Direction + ja-JP: ディレクション + type: text + limit: 100 + required: true + - field: timeout + name: + zh-CN: 超时时间 + en-US: Timeout + ja-JP: タイムアウト + type: number + range: '[0,100000]' + required: true + defaultValue: 1000 + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启用SFTP + en-US: SFTP + ja-JP: SFTP利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 概要 + en-US: Basic + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: isActive + type: 1 + i18n: + zh-CN: 活动状态 + en-US: Is Active + ja-JP: 活動ステータス + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ftp + # the config content when protocol is ftp + ftp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + direction: ^_^direction^_^ + timeout: ^_^timeout^_^ + ssl: ^_^ssl^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-fullsite.yml b/hertzbeat-manager/src/main/resources/define/app-fullsite.yml new file mode 100644 index 0000000..23b6a2c --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-fullsite.yml @@ -0,0 +1,128 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: fullsite +# The monitoring i18n name +name: + zh-CN: SiteMap全站 + en-US: SITE MAP + ja-JP: SITE MAP +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对网站全部页面的 URL 路径、HTTP 状态码、响应时间及请求反馈进行监测。由于一个网站可能有多个不同服务提供的页面,系统将采集网站暴露的<a class='help_module_content' href='https://en.wikipedia.org/wiki/Site_map'>网站地图(SiteMap)</a>来监控全站。<span class='help_module_span'><br>⚠️注意:此功能需要您的网站支持 XML 和 TXT 格式的 SiteMap。</span> + en-US: HertzBeat monitoring all pages of website by URL path, HTTP status code, response times and request feedback. Due to the possibility that a website may have multiple pages provided by different services, Hertzbeat will collect <a class='help_module_content' href='https://en.wikipedia.org/wiki/Site_map'>SiteMap</a> which exposed by the website to monitor the entire site. <span class='help_module_span'><br>⚠️Note:HertzBeat support SiteMap in XML or TXT format.</span> + zh-TW: HertzBeat對網站全部頁面的URL路徑、HTTP狀態碼、回應時間及請求迴響進行監測。 由於一個網站可能有多個不同服務提供的頁面,系統將採集網站暴露的<a class='help_module_content' href='https://en.wikipedia.org/wiki/Site_map'>網站地圖(SiteMap)</a>來監控全站。<span class='help_module_span'> <br>⚠️注意:此功能需要您的網站支持XML和TXT格式的SiteMap。</span> + ja-JP: HertzBeat はウェブサイトの全てのURL、HTTPステータスコード、応答時間などのメトリクスを監視します。ウェブサイトには、異なるサービスによって提供される複数のページが存在する可能性があるため、システムはウェブサイトによって公開される<a class='help_module_content' href='https://en.wikipedia.org/wiki/Site_map'>SiteMap</a>を収集して監視します。<span class='help_module_span'> <br>⚠️注意:この機能を使用するには、ウェブサイトがXMLおよびTXT形式のSiteMapをサポートしている必要があります。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/guide/ + en-US: https://hertzbeat.apache.org/docs/help/guide/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + defaultValue: 80 + - field: sitemap + name: + zh-CN: 网站地图 + en-US: Site Map + ja-JP: Site Map + type: text + limit: 200 + required: true + placeholder: 'Web SITEMAP EG:/sitemap.xml' + - field: ssl + name: + zh-CN: 启用HTTPS + en-US: HTTPS + ja-JP: HTTPS利用 + # type-param field type(boolean mapping the html switch tag) + type: boolean + required: true + +# collect metrics config list +metrics: + # metrics - summary + - name: summary + i18n: + zh-CN: 概要信息 + en-US: Summary + ja-JP: 概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: url + type: 1 + label: true + i18n: + zh-CN: URL + en-US: URL + ja-JP: URL + - field: statusCode + type: 1 + i18n: + zh-CN: 响应状态码 + en-US: Status Code + ja-JP: ステータスコード + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: errorMsg + type: 1 + i18n: + zh-CN: 错误信息 + en-US: Error Msg + ja-JP: エラーメッセージ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^sitemap^_^ + method: GET + ssl: ^_^ssl^_^ + parseType: sitemap diff --git a/hertzbeat-manager/src/main/resources/define/app-greenplum.yml b/hertzbeat-manager/src/main/resources/define/app-greenplum.yml new file mode 100644 index 0000000..b05b032 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-greenplum.yml @@ -0,0 +1,837 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: greenplum +# The monitoring i18n name +name: + zh-CN: GreenPlum 数据库 + en-US: GreenPlum DB + ja-JP: GreenPlum データベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 GreenPlum 数据库的通用性能指标 (basic、state、activity etc) 进行采集监控,支持版本为 GreenPlum 6.23.0+。<br>您可以点击“<i>新建 GreenPlum 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC Protocol</a> to configure SQL for collecting general metrics of GreenPlum database (basic、state、activity etc). Supported version is GreenPlum 6.23.0+. <br>You can click "<i>New GreenPlum Database</i>" and configure it, or select "<i>More Action</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 協議</a> 通過配置 SQL 對 GreenPlum 數據庫的通用性能指標 (basic、state、activity etc)進行采集監控,支持版本爲 GreenPlum 6.23.0+。<br>您可以點擊“<i>新建 GreenPlum 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBCプロトコルを介して</a> GreenPlum データベース(6.23.0+)の一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 GreenPlum データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/greenplum + en-US: https://hertzbeat.apache.org/docs/help/greenplum +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 5432 + - field: timeout + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + type: number + range: '[400,200000]' + required: false + hide: true + defaultValue: 6000 + - field: database + name: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + type: text + defaultValue: postgres + required: false + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + - field: url + name: + zh-CN: URL + en-US: URL + ja-JP: URL + type: text + required: false + hide: true + +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 基本信息 + en-US: Basic Info + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: server_version + type: 1 + label: true + i18n: + zh-CN: 服务器版本 + en-US: Server Version + ja-JP: サーバーのバージョン + - field: port + type: 1 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: server_encoding + type: 1 + i18n: + zh-CN: 服务器编码 + en-US: Server Encoding + ja-JP: サーバーのエンコード + - field: data_directory + type: 1 + i18n: + zh-CN: 数据目录 + en-US: Data Directory + ja-JP: データディレクトリ + - field: max_connections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # host: ipv4 ipv6 host + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + # database platform name + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + # sql + sql: select name, setting as value from pg_settings where name = 'max_connections' or name = 'server_version' or name = 'server_encoding' or name = 'port' or name = 'data_directory'; + # JDBC url + url: ^_^url^_^ + + - name: state + i18n: + zh-CN: 状态信息 + en-US: State Info + ja-JP: 状態情報 + priority: 1 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: conflicts + type: 0 + unit: times + i18n: + zh-CN: 冲突次数 + en-US: Conflicts + ja-JP: コンフリクト回数 + - field: deadlocks + type: 0 + unit: times + i18n: + zh-CN: 死锁次数 + en-US: Deadlocks + ja-JP: デッドロック回数 + - field: blks_read + type: 0 + unit: blocks per second + i18n: + zh-CN: 读取块 + en-US: Blocks Read + ja-JP: 読み取られたブロック + - field: blks_hit + type: 0 + unit: blocks per second + i18n: + zh-CN: 命中块 + en-US: Blocks Hit + ja-JP: ヒットブロック + - field: blk_read_time + type: 0 + unit: ms + i18n: + zh-CN: 读取时间 + en-US: Read Time + ja-JP: 読み取られタイム + - field: blk_write_time + type: 0 + unit: ms + i18n: + zh-CN: 写入时间 + en-US: Write Time + ja-JP: 書き込まれ時間 + - field: stats_reset + type: 1 + i18n: + zh-CN: 统计重置 + en-US: Stats Reset + ja-JP: 統計リセット + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: SELECT COALESCE(datname,'shared-object') as db_name, conflicts, deadlocks, blks_read, blks_hit, blk_read_time, blk_write_time, stats_reset from pg_stat_database where (datname != 'template1' and datname != 'template0') or datname is null; + url: ^_^url^_^ + + - name: activity + i18n: + zh-CN: 活动信息 + en-US: Activity Info + ja-JP: 活動情報 + priority: 2 + fields: + - field: running + type: 0 + unit: sbc + i18n: + zh-CN: 运行中 + en-US: Running + ja-JP: 実行中 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: SELECT count(*) as running FROM pg_stat_activity WHERE NOT pid=pg_backend_pid(); + url: ^_^url^_^ + + - name: resource_config + i18n: + zh-CN: 资源配置 + en-US: Resource Config + ja-JP: リソース設定 + priority: 3 + fields: + - field: work_mem + type: 0 + unit: MB + i18n: + zh-CN: 工作内存 + en-US: Work Memory + ja-JP: ワークメモリ + - field: shared_buffers + type: 0 + unit: MB + i18n: + zh-CN: 共享缓冲区 + en-US: Shared Buffers + ja-JP: 共有バッファ + - field: autovacuum + type: 1 + i18n: + zh-CN: 自动清理 + en-US: Auto Vacuum + ja-JP: オートバキューム + - field: max_connections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + - field: effective_cache_size + type: 0 + unit: MB + i18n: + zh-CN: 有效缓存大小 + en-US: Effective Cache Size + ja-JP: キャッシュサイズ + - field: wal_buffers + type: 0 + unit: MB + i18n: + zh-CN: WAL缓冲区 + en-US: WAL Buffers + ja-JP: WALバッファ + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: columns + sql: show all; + url: ^_^url^_^ + + - name: connection + i18n: + zh-CN: 连接信息 + en-US: Connection Info + ja-JP: 接続情報 + priority: 4 + fields: + - field: active + type: 0 + i18n: + zh-CN: 活动连接 + en-US: Active Connection + ja-JP: 活躍的な接続 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: select count(1) as active from pg_stat_activity; + url: ^_^url^_^ + + - name: connection_state + i18n: + zh-CN: 连接状态 + en-US: Connection State + ja-JP: 接続状態 + priority: 5 + fields: + - field: state + type: 1 + label: true + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + - field: num + type: 0 + i18n: + zh-CN: 数量 + en-US: Num + ja-JP: 数量 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select COALESCE(state, 'other') as state, count(*) as num from pg_stat_activity group by state; + url: ^_^url^_^ + + - name: connection_db + i18n: + zh-CN: 连接数据库 + en-US: Connection Db + ja-JP: 接続データベース + priority: 6 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: active + type: 0 + i18n: + zh-CN: 活动连接 + en-US: Active Connection + ja-JP: 活躍的な接続 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select count(*) as active, COALESCE(datname, 'other') as db_name from pg_stat_activity group by datname; + url: ^_^url^_^ + + - name: tuple + i18n: + zh-CN: 元组信息 + en-US: Tuple Info + ja-JP: 組情報 + priority: 7 + fields: + - field: fetched + type: 0 + i18n: + zh-CN: 获取次数 + en-US: Fetched + ja-JP: フェッチ回数 + - field: returned + type: 0 + i18n: + zh-CN: 返回次数 + en-US: Returned + ja-JP: 戻る回数 + - field: inserted + type: 0 + i18n: + zh-CN: 插入次数 + en-US: Inserted + ja-JP: インサート回数 + - field: updated + type: 0 + i18n: + zh-CN: 更新次数 + en-US: Updated + ja-JP: 更新回数 + - field: deleted + type: 0 + i18n: + zh-CN: 删除次数 + en-US: Deleted + ja-JP: 削除回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select sum(tup_fetched) as fetched, sum(tup_updated) as updated, sum(tup_deleted) as deleted, sum(tup_inserted) as inserted, sum(tup_returned) as returned from pg_stat_database; + url: ^_^url^_^ + + - name: temp_file + i18n: + zh-CN: 临时文件 + en-US: Temp File + ja-JP: 一時ファイル + priority: 8 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: num + type: 0 + i18n: + zh-CN: 次数 + en-US: Num + ja-JP: 数量 + - field: Size + type: 0 + unit: B + i18n: + zh-CN: 大小 + en-US: Size + ja-JP: サイズ + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select COALESCE(datname, 'other') as db_name, sum(temp_files) as num, sum(temp_bytes) as size from pg_stat_database group by datname; + url: ^_^url^_^ + + - name: lock + i18n: + zh-CN: 锁信息 + en-US: Lock Info + ja-JP: ロック情報 + priority: 9 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: conflicts + type: 0 + unit: times + i18n: + zh-CN: 冲突次数 + en-US: Conflicts + ja-JP: コンフリクト回数 + - field: deadlocks + type: 0 + unit: times + i18n: + zh-CN: 死锁次数 + en-US: Deadlocks + ja-JP: デッドロック回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: SELECT COALESCE(datname,'shared-object') as db_name, conflicts, deadlocks from pg_stat_database where (datname != 'template1' and datname != 'template0') or datname is null; + url: ^_^url^_^ + + - name: slow_sql + i18n: + zh-CN: 慢查询 + en-US: Slow Sql + ja-JP: スローSQL + priority: 10 + fields: + - field: sql_text + type: 1 + label: true + i18n: + zh-CN: SQL语句 + en-US: SQL Text + ja-JP: SQL文のテキスト + - field: calls + type: 0 + i18n: + zh-CN: 调用次数 + en-US: Calls + ja-JP: コール回数 + - field: rows + type: 0 + i18n: + zh-CN: 行数 + en-US: Rows + ja-JP: 行 + - field: avg_time + type: 0 + unit: ms + i18n: + zh-CN: 平均时间 + en-US: Avg Time + ja-JP: 平均時間 + - field: total_time + type: 0 + unit: ms + i18n: + zh-CN: 总时间 + en-US: Total Time + ja-JP: 合計時間 + aliasFields: + - query + - calls + - rows + - total_exec_time + - mean_exec_time + calculates: + - sql_text=query + - avg_time=mean_exec_time + - total_time=total_exec_time + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select * from pg_stat_statements; + url: ^_^url^_^ + + - name: transaction + i18n: + zh-CN: 事务信息 + en-US: Transaction Info + ja-JP: トランザクション情報 + priority: 11 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: commits + type: 0 + unit: times + i18n: + zh-CN: 提交次数 + en-US: Commits + ja-JP: コミット回数 + - field: rollbacks + type: 0 + unit: times + i18n: + zh-CN: 回滚次数 + en-US: Rollbacks + ja-JP: ロールバック回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select COALESCE(datname, 'other') as db_name, sum(xact_commit) as commits, sum(xact_rollback) as rollbacks from pg_stat_database group by datname; + url: ^_^url^_^ + + - name: conflicts + i18n: + zh-CN: 冲突信息 + en-US: Conflicts Info + ja-JP: コンフリクト情報 + priority: 12 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: tablespace + type: 0 + i18n: + zh-CN: 表空间 + en-US: Tablespace + ja-JP: 表領域 + - field: lock + type: 0 + i18n: + zh-CN: 锁 + en-US: Lock + ja-JP: ロック + - field: snapshot + type: 0 + i18n: + zh-CN: 快照 + en-US: Snapshot + ja-JP: スナップショット + - field: bufferpin + type: 0 + i18n: + zh-CN: 缓冲区 + en-US: Bufferpin + ja-JP: バッファ + - field: deadlock + type: 0 + i18n: + zh-CN: 死锁 + en-US: Deadlock + ja-JP: デッドロック + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select datname as db_name, confl_tablespace as tablespace, confl_lock as lock, confl_snapshot as snapshot, confl_bufferpin as bufferpin, confl_deadlock as deadlock from pg_stat_database_conflicts; + url: ^_^url^_^ + + - name: cache_hit_ratio + i18n: + zh-CN: 缓存命中率 + en-US: Cache Hit Ratio + ja-JP: キャッシュ命中率 + priority: 13 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: ratio + type: 0 + unit: '%' + i18n: + zh-CN: 命中率 + en-US: Hit Ratio + ja-JP: 命中率 + aliasFields: + - blks_hit + - blks_read + - db_name + calculates: + - ratio=(blks_hit + 1) / (blks_read + blks_hit + 1) * 100 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select datname as db_name, blks_hit, blks_read from pg_stat_database; + url: ^_^url^_^ + + - name: checkpoint + i18n: + zh-CN: Checkpoint信息 + en-US: Checkpoint Info + ja-JP: チェックポイント情報 + priority: 14 + fields: + - field: checkpoint_sync_time + type: 0 + unit: ms + i18n: + zh-CN: Checkpoint同步时间 + en-US: Checkpoint Sync Time + ja-JP: チェックポイント同期時間 + - field: checkpoint_write_time + type: 0 + unit: ms + i18n: + zh-CN: Checkpoint写入时间 + en-US: Checkpoint Write Time + ja-JP: Checkpoint書き込まれた時間 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: select checkpoint_sync_time, checkpoint_write_time from pg_stat_bgwriter; + url: ^_^url^_^ + + - name: buffer + i18n: + zh-CN: Buffer信息 + en-US: Buffer Info + ja-JP: バッファ情報 + priority: 15 + fields: + - field: allocated + type: 0 + i18n: + zh-CN: 已分配 + en-US: Allocated + ja-JP: 割り当てバッファ + - field: fsync_calls_by_backend + type: 0 + i18n: + zh-CN: 后端进程直接执行的文件同步调用次数 + en-US: Fsync Calls By Backend + ja-JP: バックエンド同期コール回数 + - field: written_directly_by_backend + type: 0 + i18n: + zh-CN: 后台写入到数据文件 + en-US: Written Directly By Backend + ja-JP: バックエンドによる直接書き込まれたファイル + - field: written_by_background_writer + type: 0 + i18n: + zh-CN: 后台写入 + en-US: Written By Background Writer + ja-JP: バックグラウンドライターに書き込まれた + - field: written_during_checkpoints + type: 0 + i18n: + zh-CN: 检查点期间写入 + en-US: Written During Checkpoints + ja-JP: チェックポイント中の書き込み + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: select buffers_alloc as allocated, buffers_backend_fsync as fsync_calls_by_backend, buffers_backend as written_directly_by_backend, buffers_clean as written_by_background_writer, buffers_checkpoint as written_during_checkpoints from pg_stat_bgwriter; + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-greptime.yml b/hertzbeat-manager/src/main/resources/define/app-greptime.yml new file mode 100644 index 0000000..78cc505 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-greptime.yml @@ -0,0 +1,724 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: greptime +# The monitoring i18n name +name: + zh-CN: GreptimeDB + en-US: GreptimeDB + ja-JP: GreptimeDB +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 GreptimeDB 时序数据库进行监控。<br><span class='help_module_span'><a class='help_module_content' https://docs.greptime.com/user-guide/operations/monitoring'>点击查看开启步骤</a>。</span> + en-US: HertzBeat monitors the GreptimeDB time series database. <br><span class='help_module_span'><a class='help_module_content' https://docs.greptime.com/user-guide/operations/monitoring'>Click to view the activation steps</a>. </span> + zh-TW: HertzBeat 對 GreptimeDB 時序資料庫進行監控。<br><span class='help_module_span'><a class='help_module_content' https://docs.greptime.com/user-guide/operations/monitoring'>點擊查看開啓步驟</a>。</span> + ja-JP: HertzBeat は GreptimeDB 時系列データベースを監視します。<br><span class='help_module_span'><a class='help_module_content' https://docs.greptime.com/user-guide/operations/monitoring'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/greptimedb + en-US: https://hertzbeat.apache.org/docs/help/greptimedb +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # default value + defaultValue: 4000 + required: true + - field: timeout + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + type: number + required: false + # hide param-true or false + hide: true + defaultValue: 6000 + +# collect metrics config list +metrics: + # metrics - cluster_node_status + - name: greptime_app_version + i18n: + zh-CN: greptime 应用版本 + en-US: greptime_app_version + ja-JP: greptime応用バージョン + priority: 0 + fields: + - field: short_version + i18n: + zh-CN: 版本 + en-US: version + ja-JP: バージョン + type: 1 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: greptime_catalog_schema_count + i18n: + zh-CN: 目录 模式 数量 + en-US: greptime_catalog_schema_count + ja-JP: greptime_catalog_schema_count + priority: 1 + fields: + - field: value + i18n: + zh-CN: 数量 + en-US: count + ja-JP: 数量 + type: 1 + calculates: + - name=.name + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: greptime_meta_cache_container_cache_get + i18n: + zh-CN: 缓存容器缓存获取 + en-US: greptime_meta_cache_container_cache_get + ja-JP: キャッシュゲット + priority: 2 + fields: + - field: name + i18n: + zh-CN: 名称 + en-US: name + ja-JP: キー + type: 1 + label: true + - field: value + i18n: + zh-CN: 指标值 + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: greptime_meta_cache_container_cache_miss + i18n: + zh-CN: 缓存容器缓存未命中 + en-US: greptime_meta_cache_container_cache_miss + ja-JP: キャッシュミス + priority: 3 + fields: + - field: name + i18n: + zh-CN: 名称 + en-US: name + ja-JP: 名前 + type: 1 + label: true + - field: value + i18n: + zh-CN: 指标值 + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: greptime_mito_region_count + i18n: + zh-CN: mito 引擎区域数量 + en-US: greptime_mito_region_count + ja-JP: mitoエンジンのリージョン数量 + priority: 4 + fields: + - field: worker + i18n: + zh-CN: 工作线程 + en-US: worker + ja-JP: ワーカースレッド + type: 1 + label: true + - field: value + i18n: + zh-CN: 数量 + en-US: count + ja-JP: 数量 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: greptime_mito_write_stall_total + i18n: + zh-CN: mito 引擎写入延迟总数 + en-US: greptime_mito_write_stall_total + ja-JP: mitoエンジンの書き込み遅延の合計 + priority: 5 + fields: + - field: worker + i18n: + zh-CN: 工作线程 + en-US: worker + ja-JP: ワーカースレッド + type: 1 + label: true + - field: value + i18n: + zh-CN: 数量 + en-US: total + ja-JP: 数量 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: greptime_meta_create_catalog_counter + i18n: + zh-CN: 创建目录计数器 + en-US: greptime_meta_create_catalog_counter + ja-JP: 目録カウンター + priority: 6 + fields: + - field: value + i18n: + zh-CN: 指标值 + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: greptime_runtime_threads_alive + i18n: + zh-CN: 运行时线程存活 + en-US: greptime_runtime_threads_alive + ja-JP: ランタイムのアライブスレッド + priority: 7 + fields: + - field: thread_name + i18n: + zh-CN: 线程名称 + en-US: thread_name + ja-JP: スレッド名 + type: 1 + - field: value + i18n: + zh-CN: value + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: greptime_runtime_threads_idle + i18n: + zh-CN: 运行时线程空闲 + en-US: greptime_runtime_threads_idle + ja-JP: ランタイムのidleスレッド + priority: 8 + fields: + - field: thread_name + i18n: + zh-CN: 线程名称 + en-US: thread_name + ja-JP: スレッド名 + type: 1 + - field: value + i18n: + zh-CN: value + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: greptime_servers_http_requests_total + i18n: + zh-CN: greptime 服务 HTTP 请求总数 + en-US: greptime_servers_http_requests_total + ja-JP: HTTPリクエストの合計 + priority: 9 + fields: + - field: code + i18n: + zh-CN: code + en-US: code + ja-JP: コード + type: 1 + - field: method + i18n: + zh-CN: method + en-US: method + ja-JP: メソッド + type: 1 + - field: path + i18n: + zh-CN: path + en-US: path + ja-JP: パス + type: 1 + - field: value + i18n: + zh-CN: value + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: greptime_servers_mysql_connection_count + i18n: + zh-CN: greptime 服务 MySQL 连接数 + en-US: greptime_servers_mysql_connection_count + ja-JP: MySQLの接続数 + priority: 10 + fields: + - field: value + i18n: + zh-CN: value + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: greptime_servers_postgres_connection_count + i18n: + zh-CN: greptime 服务 Postgres 连接数 + en-US: greptime_servers_postgres_connection_count + ja-JP: Postgresの接続数 + priority: 11 + fields: + - field: value + i18n: + zh-CN: value + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: process_cpu_seconds_total + i18n: + zh-CN: 进程 CPU 时间总数 + en-US: process_cpu_seconds_total + ja-JP: CPU時間合計 + priority: 12 + fields: + - field: value + i18n: + zh-CN: value + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: process_max_fds + i18n: + zh-CN: 进程最大文件描述符 + en-US: process_max_fds + ja-JP: プロセスの最大ファイル記述子 + priority: 13 + fields: + - field: value + i18n: + zh-CN: value + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: process_open_fds + i18n: + zh-CN: 进程打开文件描述符 + en-US: process_open_fds + ja-JP: プロセスのオープン中ファイル記述子 + priority: 14 + fields: + - field: value + i18n: + zh-CN: value + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: process_resident_memory_bytes + i18n: + zh-CN: 进程常驻内存字节 + en-US: process_resident_memory_bytes + ja-JP: プロセスの常駐メモリバイト + priority: 15 + fields: + - field: value + i18n: + zh-CN: value + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: process_start_time_seconds + i18n: + zh-CN: 进程启动时间(秒) + en-US: process_start_time_seconds + ja-JP: プロセスのアップタイム(秒) + priority: 16 + fields: + - field: value + i18n: + zh-CN: value + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: process_threads + i18n: + zh-CN: 进程线程 + en-US: process_threads + ja-JP: プロセススレッド + priority: 17 + fields: + - field: value + i18n: + zh-CN: value + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: process_virtual_memory_bytes + i18n: + zh-CN: 进程虚拟内存字节 + en-US: process_virtual_memory_bytes + ja-JP: プロセスの仮想メモリバイト + priority: 18 + fields: + - field: value + i18n: + zh-CN: value + en-US: value + ja-JP: 値 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: raft_engine_log_entry_count + i18n: + zh-CN: raft 引擎日志条目数量 + en-US: raft_engine_log_entry_count + ja-JP: raftエンジンのログエントリー数 + priority: 19 + fields: + - field: type + i18n: + zh-CN: 类型 + en-US: type + ja-JP: タイプ + type: 1 + - field: value + i18n: + zh-CN: 数量 + en-US: count + ja-JP: 数量 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: raft_engine_log_file_count + i18n: + zh-CN: raft 引擎日志文件数量 + en-US: raft_engine_log_file_count + ja-JP: raftエンジンのログファイル数 + priority: 20 + fields: + - field: type + i18n: + zh-CN: 类型 + en-US: type + ja-JP: タイプ + type: 1 + - field: value + i18n: + zh-CN: 数量 + en-US: count + ja-JP: 数量 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: raft_engine_memory_usage + i18n: + zh-CN: raft 引擎内存占用 + en-US: raft_engine_memory_usage + ja-JP: raftエンジンのメモリ使用率 + priority: 21 + fields: + - field: value + i18n: + zh-CN: 占用 + en-US: total + ja-JP: 使用率 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: raft_engine_recycled_file_count + i18n: + zh-CN: raft 引擎回收文件数量 + en-US: raft_engine_recycled_file_count + ja-JP: raftエンジンのリサイクルファイル数 + priority: 22 + fields: + - field: type + i18n: + zh-CN: 类型 + en-US: type + ja-JP: タイプ + type: 1 + - field: value + i18n: + zh-CN: 数量 + en-US: count + ja-JP: 数量 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: sys_jemalloc_allocated + i18n: + zh-CN: jemalloc 分配 + en-US: sys_jemalloc_allocated + ja-JP: jemallocの割り当て + priority: 23 + fields: + - field: value + i18n: + zh-CN: 数量 + en-US: value + ja-JP: 数量 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + # metrics - cluster_node_status + - name: sys_jemalloc_resident + i18n: + zh-CN: jemalloc 常驻 + en-US: sys_jemalloc_resident + ja-JP: jemallocの常駐 + priority: 24 + fields: + - field: value + i18n: + zh-CN: 数量 + en-US: value + ja-JP: 数量 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus diff --git a/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml b/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml new file mode 100644 index 0000000..4fb6360 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml @@ -0,0 +1,463 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: network +# The monitoring type eg: linux windows tomcat mysql aws... +app: h3c_switch +# The monitoring i18n name +name: + zh-CN: 华三通用交换机 + en-US: H3C Switch + ja-JP: H3Cスイッチングハブ +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 协议</a> 对 华三交换机 的通用指标(可用性,系统信息,端口流量等)进行采集监控。<br>您可以点击 “<i>新建 华三通用交换机</i>” 并进行配置SNMP相关参数添加,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP Protocol</a> to monitoring H3C Switch general performance metrics. <br>You can click the "<i>New H3C Switch</i>" button and config snmp params to add monitor or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 協議</a> 對 華三交換機 的通用指標(可用性,系統信息,端口流量等)進行采集監控。<br>您可以點擊 “<i>新建 華三通用交換機</i>” 並進行配置SNMP相關參數添加,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP プロトコルを介して</a> H3Cスイッチングハブの一般的なメトリクスを監視します。<br>「<i>新規 H3Cスイッチングハブ</i>」をクリックしてSNMPなどのパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/h3c_switch + en-US: https://hertzbeat.apache.org/docs/help/h3c_switch +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 161 + # field-param field key + - field: snmpVersion + # name-param field display i18n name + name: + zh-CN: SNMP 版本 + en-US: SNMP Version + ja-JP: SNMPバージョン + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: SNMPv1 + value: 0 + - label: SNMPv2c + value: 1 + - label: SNMPv3 + value: 3 + # field-param field key + - field: community + # name-param field display i18n name + name: + zh-CN: SNMP 团体字 + en-US: SNMP Community + ja-JP: SNMPコミュニティ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp community for v1 v2c' + # dependent parameter values list + depend: + snmpVersion: + - 0 + - 1 + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: SNMP username + en-US: SNMP username + ja-JP: SNMPユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp username for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: contextName + # name-param field display i18n name + name: + zh-CN: SNMP contextName + en-US: SNMP contextName + ja-JP: SNMPコンテキスト名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp contextName for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP authPassword + en-US: SNMP authPassword + ja-JP: SNMP認証パスワード + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPasswordEncryption + # name-param field display i18n name + name: + zh-CN: authPassword 加密方式 + en-US: authPassword Encryption + ja-JP: 認証暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: MD5 + value: 0 + - label: SHA1 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP privPassphrase + en-US: SNMP privPassphrase + ja-JP: SNMPパスワードフレーズ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPasswordEncryption + # name-param field display i18n name + name: + zh-CN: privPassword 加密方式 + en-US: privPassword Encryption + ja-JP: パスワードの暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: DES + value: 0 + - label: AES128 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: false + # hide-is hide this field and put it in advanced layout + hide: true + # default value + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - system + - name: system + i18n: + zh-CN: 系统信息 + en-US: System Info + ja-JP: システム情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: name + type: 1 + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: descr + type: 1 + i18n: + zh-CN: 描述信息 + en-US: Description + ja-JP: 説明 + - field: uptime + type: 1 + i18n: + zh-CN: 运行时长 + en-US: Uptime + ja-JP: アップタイム + - field: location + type: 1 + i18n: + zh-CN: 位置 + en-US: Location + ja-JP: 位置 + - field: contact + type: 1 + i18n: + zh-CN: 联系人 + en-US: Contact + ja-JP: 連絡先 + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: snmp + # the config content when protocol is snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + # snmp 团体字 + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: get + # metrics oids: metric_name - oid_value + oids: + name: 1.3.6.1.2.1.1.5.0 + descr: 1.3.6.1.2.1.1.1.0 + uptime: 1.3.6.1.2.1.1.3.0 + location: 1.3.6.1.2.1.1.6.0 + contact: 1.3.6.1.2.1.1.4.0 + + - name: interfaces + i18n: + zh-CN: 接口详情 + en-US: Interfaces Detail + ja-JP: ネットワークカード詳細 + priority: 1 + fields: + - field: index + type: 1 + i18n: + zh-CN: 编号 + en-US: Index + ja-JP: 番号 + - field: descr + type: 1 + label: true + i18n: + zh-CN: 接口名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: mtu + type: 0 + unit: 'byte' + i18n: + zh-CN: MTU + en-US: MTU + ja-JP: MTU + - field: speed + type: 0 + unit: 'MB/s' + i18n: + zh-CN: 接口速率 + en-US: Interface Speed + ja-JP: ネットワークカード速度 + - field: in_octets + type: 0 + unit: 'MByte' + i18n: + zh-CN: 入流量 + en-US: In Octets + ja-JP: 受信されたバイト数 + - field: in_discards + type: 0 + i18n: + zh-CN: 入丢包数 + en-US: In Discards + ja-JP: 受信されたパケットのロス数 + - field: in_errors + type: 0 + i18n: + zh-CN: 入错包数 + en-US: In Errors + ja-JP: 受信異常パケット数 + - field: out_octets + type: 0 + unit: 'MByte' + i18n: + zh-CN: 出流量 + en-US: Out Octets + ja-JP: 転送されたバイト数 + - field: out_discards + type: 0 + i18n: + zh-CN: 出丢包数 + en-US: Out Discards + ja-JP: 転送されたパケットのロス数 + - field: out_errors + type: 0 + i18n: + zh-CN: 出错包数 + en-US: Out Errors + ja-JP: 転送異常パケット数 + - field: admin_status + type: 1 + i18n: + zh-CN: 配置状态 + en-US: Config Status + ja-JP: 設定ステータス + - field: oper_status + type: 1 + i18n: + zh-CN: 当前状态 + en-US: Current Status + ja-JP: 現在のステータス + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - ifIndex + - ifDescr + - ifMtu + - ifSpeed + - ifInOctets + - ifInDiscards + - ifInErrors + - ifOutOctets + - ifOutDiscards + - ifOutErrors + - ifAdminStatus + - ifOperStatus + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - index=ifIndex + - descr=ifDescr + - mtu=ifMtu + - speed=ifSpeed / 1000 / 1000 + - in_octets=ifInOctets / 1000 / 1000 + - in_discards=ifInDiscards + - in_errors=ifInErrors + - out_octets=ifOutOctets / 1000 / 1000 + - out_discards=ifOutDiscards + - out_errors=ifOutErrors + - admin_status=ifAdminStatus + - oper_status=ifOperStatus + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + authPassphrase: ^_^authPassphrase^_^ + privPassphrase: ^_^privPassphrase^_^ + privPasswordEncryption: ^_^privPasswordEncryption^_^ + authPasswordEncryption: ^_^authPasswordEncryption^_^ + contextName: ^_^contextName^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + ifIndex: 1.3.6.1.2.1.2.2.1.1 + ifDescr: 1.3.6.1.2.1.2.2.1.2 + ifMtu: 1.3.6.1.2.1.2.2.1.4 + ifSpeed: 1.3.6.1.2.1.2.2.1.5 + ifInOctets: 1.3.6.1.2.1.2.2.1.10 + ifInDiscards: 1.3.6.1.2.1.2.2.1.13 + ifInErrors: 1.3.6.1.2.1.2.2.1.14 + ifOutOctets: 1.3.6.1.2.1.2.2.1.16 + ifOutDiscards: 1.3.6.1.2.1.2.2.1.19 + ifOutErrors: 1.3.6.1.2.1.2.2.1.20 + ifAdminStatus: 1.3.6.1.2.1.2.2.1.7 + ifOperStatus: 1.3.6.1.2.1.2.2.1.8 diff --git a/hertzbeat-manager/src/main/resources/define/app-hadoop.yml b/hertzbeat-manager/src/main/resources/define/app-hadoop.yml new file mode 100644 index 0000000..c4091d6 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-hadoop.yml @@ -0,0 +1,376 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: hadoop +# The monitoring i18n name +name: + zh-CN: Apache Hadoop + en-US: Apache Hadoop + ja-JP: Apache Hadoop +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用<a class='help_module_content' href='https://baijiahao.baidu.com/s?id=1605937053950156833&wfr=spider&for=pc'> JMX 协议</a>对 Hadoop 的 JVM 虚拟机的通用性能指标(memory pool,限JDK8及以下的code cache、class loading、thread)进行采集监控。<br><span class='help_module_span'>⚠️注意:您需要在 Hadoop 应用开启 JMX 服务, <a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/hadoop#hadoop%E5%BA%94%E7%94%A8%E5%BC%80%E5%90%AFjmx%E5%8D%8F%E8%AE%AE%E6%AD%A5%E9%AA%A4'>点击查看开启步骤</a>。</span> + en-US: "HertzBeat monitors general performance metrics(memory pool, class loading, thread) of Hadoop VMware through <a class='help_module_content' href='https://zh.wikipedia.org/JMX'>JMX protocol</a>. <br><span class='help_module_span'>⚠️Note: You should enable the JMX service in Hadoop application, and the metric of code cache is only available to JDK8 and below.<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/hadoop#hadoop%E5%BA%94%E7%94%A8%E5%BC%80%E5%90%AFjmx%E5%8D%8F%E8%AE%AE%E6%AD%A5%E9%AA%A4'>Click here to view the specific steps.</a></span>" + zh-TW: HertzBeat使用<a class='help_module_content' href='https://baijiahao.baidu.com/s?id=1605937053950156833&wfr=spider&for=pc'> JMX協定</a>對Hadoop的JVM虛擬機器的通用性能指標(memory pool,限JDK8及以下的code cache、class loading、thread)進行採集監控。<br><span class='help_module_span'> ⚠️ ️注意:您需要在Hadoop應用開啟JMX服務,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/hadoop#hadoop%E5%BA%94%E7%94%A8%E5%BC%80%E5%90%AFjmx%E5%8D%8F%E8%AE%AE%E6%AD%A5%E9%AA%A4'>點擊查看開啟步驟</a>。</span> + ja-JP: HertzBeatは <a class='help_module_content' href='https://baijiahao.baidu.com/s?id=1605937053950156833&wfr=spider&for=pc'> JMXプロトコルを介して</a> HadoopのJava仮想マシンの一般的なパフォーマンスのメトリクスを監視します。<br><span class='help_module_span'> ⚠️注意:Hadoop で JMX サービスを有効にする必要があります。<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/hadoop#hadoop%E5%BA%94%E7%94%A8%E5%BC%80%E5%90%AFjmx%E5%8D%8F%E8%AE%AE%E6%AD%A5%E9%AA%A4'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/hadoop/ + en-US: https://hertzbeat.apache.org/docs/help/hadoop/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 9999 + # field-param field key + - field: url + # name-param field display i18n name + name: + zh-CN: JMX URL + en-US: JMX URL + ja-JP: JMX URL + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + # param field input placeholder + placeholder: 'service:jmx:rmi:///jndi/rmi://host:port/jmxrmi' + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 虚拟机基础信息 + en-US: JVM Basic + ja-JP: Java仮想マシン基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: VmName + type: 1 + i18n: + zh-CN: 名称 + en-US: Vm Name + ja-JP: 仮想マシン名 + - field: VmVendor + type: 1 + i18n: + zh-CN: 厂商 + en-US: Vm Vendor + ja-JP: 仮想マシンベンダー + - field: VmVersion + type: 1 + i18n: + zh-CN: 版本 + en-US: Vm Version + ja-JP: 仮想マシンバージョン + - field: Uptime + type: 0 + unit: ms + i18n: + zh-CN: 运行时长 + en-US: Up time + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jmx + # the config content when protocol is jmx + jmx: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: java.lang:type=Runtime + url: ^_^url^_^ + + - name: memory_pool + priority: 1 + i18n: + zh-CN: 内存池 + en-US: Memory Pool + ja-JP: メモリプール + fields: + - field: name + type: 1 + label: true + i18n: + zh-CN: 指标名称 + en-US: Name + ja-JP: メトリクス名 + - field: committed + type: 0 + unit: MB + i18n: + zh-CN: 已分配内存 + en-US: Committed + ja-JP: コミットされたメモリ + - field: init + type: 0 + unit: MB + i18n: + zh-CN: 初始化内存 + en-US: Init + ja-JP: イニシャルメモリ + - field: max + type: 0 + unit: MB + i18n: + zh-CN: 最大内存 + en-US: Max + ja-JP: 最大メモリ + - field: used + type: 0 + unit: MB + i18n: + zh-CN: 已使用内存 + en-US: Used + ja-JP: 使用したメモリ + units: + - committed=B->MB + - init=B->MB + - max=B->MB + - used=B->MB + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - Name + - Usage->committed + - Usage->init + - Usage->max + - Usage->used + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - name=Name + - committed=Usage->committed + - init=Usage->init + - max=Usage->max + - used=Usage->used + protocol: jmx + jmx: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=MemoryPool,name=* + url: ^_^url^_^ + + - name: code_cache + priority: 2 + i18n: + zh-CN: 本地代码缓冲区 + en-US: Code Cache + ja-JP: コードキャッシュ + fields: + - field: committed + type: 0 + i18n: + zh-CN: 已分配内存 + en-US: Committed + ja-JP: コミットされたメモリ + - field: init + type: 0 + i18n: + zh-CN: 初始化内存 + en-US: Init + ja-JP: イニシャルメモリ + - field: max + type: 0 + i18n: + zh-CN: 最大内存 + en-US: Max + ja-JP: 最大メモリ + - field: used + type: 0 + i18n: + zh-CN: 已使用内存 + en-US: Used + ja-JP: 使用したメモリ + aliasFields: + - Usage->committed + - Usage->init + - Usage->max + - Usage->used + calculates: + - committed=Usage->committed + - init=Usage->init + - max=Usage->max + - used=Usage->used + protocol: jmx + jmx: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=MemoryPool,name=Code Cache + url: ^_^url^_^ + + - name: class_loading + priority: 3 + i18n: + zh-CN: 类加载信息 + en-US: Class Loading + ja-JP: クラスローディング情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: LoadedClassCount + type: 0 + i18n: + zh-CN: 当前已加载类数量 + en-US: Loaded Class Count + ja-JP: ロードされたクラス数 + - field: TotalLoadedClassCount + type: 0 + i18n: + zh-CN: 已加载类总数量 + en-US: Total Loaded Class Count + ja-JP: ロードされたクラス総数 + - field: UnloadedClassCount + type: 0 + i18n: + zh-CN: 未加载类总数量 + en-US: Unloaded Class Count + ja-JP: アンロードされたクラス総数 + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=ClassLoading + url: ^_^url^_^ + + - name: thread + priority: 4 + i18n: + zh-CN: 线程信息 + en-US: Thread + ja-JP: スレッド情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: TotalStartedThreadCount + type: 0 + i18n: + zh-CN: 已启动线程总数 + en-US: Total Started Thread Count + ja-JP: スレッド総数 + - field: ThreadCount + type: 0 + i18n: + zh-CN: 活跃线程数 + en-US: Thread Count + ja-JP: 活躍スレッド数 + - field: PeakThreadCount + type: 0 + i18n: + zh-CN: 最大峰值线程数 + en-US: Peak Thread Count + ja-JP: 最大スレッド数 + - field: DaemonThreadCount + type: 0 + i18n: + zh-CN: 活跃守护线程数 + en-US: Daemon Thread Count + ja-JP: デーモンスレッド数 + - field: CurrentThreadUserTime + type: 0 + unit: s + i18n: + zh-CN: 线程占用的CPU时间(用户态) + en-US: Current Thread User Time + ja-JP: 現在のスレッドユーザー時間 + - field: CurrentThreadCpuTime + type: 0 + unit: s + i18n: + zh-CN: 线程占用的CPU时间 + en-US: Current Thread CPU Time + ja-JP: 現在のスレッドシステム時間 + units: + - CurrentThreadUserTime=NS->S + - CurrentThreadCpuTime=NS->S + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=Threading + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-hbase_master.yml b/hertzbeat-manager/src/main/resources/define/app-hbase_master.yml new file mode 100644 index 0000000..50b2147 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-hbase_master.yml @@ -0,0 +1,292 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: hbase_master +# The monitoring i18n name +name: + zh-CN: Apache Hbase Master + en-US: Apache Hbase Master + ja-JP: Apache Hbase Master +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Hbase 数据库 Master 节点监控指标进行监控。<br>您可以点击 “<i>新建 Apache Hbase Master</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors the Master node monitoring indicators of the Hbase database. <br>You can click "<i>New Apache Hbase Master</i>" to configure, or select "<i>More Actions</i>" to import an existing configuration. + zh-TW: Hertzbeat 對 Hbase 數據庫 Master 节點監控指標進行監控。<br>您可以點擊 “<i>新建 Apache Hbase Master</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は HbaseデータベースのMasterノードの一般的なメトリクスを監視します。<br>「<i>新規 Apache Hbase Master</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/hbase_master/ + en-US: https://hertzbeat.apache.org/docs/help/hbase_master/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 16010 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启用HTTPS + en-US: HTTPS + ja-JP: HTTPS + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true +# collect metrics config list +metrics: + # metrics - Server + - name: Server + i18n: + zh-CN: Master服务信息 + en-US: Master Service Info + ja-JP: マスターサービス情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: numRegionServers + type: 0 + i18n: + zh-CN: 活跃RegionServer数量 + en-US: numRegionServers + ja-JP: 活躍的なRegionServer数 + - field: numDeadRegionServers + type: 0 + i18n: + zh-CN: 异常RegionServer数量 + en-US: numDeadRegionServers + ja-JP: 異常的なRegionServer数 + - field: averageLoad + type: 0 + i18n: + zh-CN: 集群平均负载 + en-US: averageLoad + ja-JP: 平均ロード + - field: clusterRequests + type: 0 + i18n: + zh-CN: 集群请求数量 + en-US: clusterRequests + ja-JP: クラスターのリクエスト数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.numRegionServers + - $.numDeadRegionServers + - $.averageLoad + - $.clusterRequests + calculates: + - numRegionServers=$.numRegionServers + - numDeadRegionServers=$.numDeadRegionServers + - averageLoad=$.averageLoad + - clusterRequests=$.clusterRequests + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Server")]' + - name: Rit + i18n: + zh-CN: Region In Transition 信息 + en-US: Region In Transition Info + ja-JP: Region In Transition 情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ritCount + type: 0 + i18n: + zh-CN: 当前的 RIT 数量 + en-US: ritCount + ja-JP: RIT数 + - field: ritCountOverThreshold + type: 0 + i18n: + zh-CN: 超过阈值的 RIT 数量 + en-US: ritCountOverThreshold + ja-JP: 閾値を超えたRIT数 + - field: ritOldestAge + type: 0 + i18n: + zh-CN: 最老的RIT的持续时间 + en-US: ritOldestAge + ja-JP: 最古のRITのスパン + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.ritCount + - $.ritCountOverThreshold + - $.ritOldestAge + calculates: + - ritCount=$.ritCount + - ritCountOverThreshold=$.ritCountOverThreshold + - ritOldestAge=$.ritOldestAge + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=AssignmentManager")]' + - name: basic + i18n: + zh-CN: 基础信息 + en-US: Basic Info + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: liveRegionServers + type: 1 + i18n: + zh-CN: 当前活跃RegionServer列表 + en-US: liveRegionServers + ja-JP: 活躍的なRegionServer + - field: deadRegionServers + type: 1 + i18n: + zh-CN: 当前离线RegionServer列表 + en-US: deadRegionServers + ja-JP: オフラインRegionServer + - field: zookeeperQuorum + type: 1 + i18n: + zh-CN: Zookeeper列表 + en-US: zookeeperQuorum + ja-JP: zookeeper定足数 + - field: masterHostName + type: 1 + i18n: + zh-CN: Master节点 + en-US: masterHostName + ja-JP: マスターホスト名 + - field: BalancerCluster_num_ops + type: 0 + i18n: + zh-CN: 集群负载均衡次数 + en-US: BalancerCluster_num_ops + ja-JP: クラスターのロードバランシング回数 + - field: numActiveHandler + type: 0 + i18n: + zh-CN: RPC句柄数 + en-US: numActiveHandler + ja-JP: RPCハンドル数 + - field: receivedBytes + type: 0 + unit: 'MB' + i18n: + zh-CN: 集群接收数据量(MB) + en-US: receivedBytes + ja-JP: 受信されたバイト + - field: sentBytes + type: 0 + unit: 'MB' + i18n: + zh-CN: 集群发送数据量(MB) + en-US: sentBytes + ja-JP: 転送されたバイト + - field: clusterRequests + type: 0 + i18n: + zh-CN: 集群总请求数量 + en-US: clusterRequests + ja-JP: クラスターのリクエスト数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Server")].['tag.liveRegionServers'] + - $.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Server")].['tag.deadRegionServers'] + - $.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Server")].['tag.zookeeperQuorum'] + - $.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Server")].['tag.Hostname'] + - $.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Balancer")].BalancerCluster_num_ops + - $.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=IPC")].numActiveHandler + - $.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=IPC")].receivedBytes + - $.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=IPC")].sentBytes + - $.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Server")].clusterRequests + calculates: + - liveRegionServers=$.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Server")].['tag.liveRegionServers'] + - deadRegionServers=$.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Server")].['tag.deadRegionServers'] + - zookeeperQuorum=$.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Server")].['tag.zookeeperQuorum'] + - masterHostName=$.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Server")].['tag.Hostname'] + - BalancerCluster_num_ops=$.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Balancer")].BalancerCluster_num_ops + - numActiveHandler=$.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=IPC")].numActiveHandler + - receivedBytes=$.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=IPC")].receivedBytes + - sentBytes=$.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=IPC")].sentBytes + - clusterRequests=$.beans[?(@.name == "Hadoop:service=HBase,name=Master,sub=Server")].clusterRequests + units: + - receivedBytes=B->MB + - sentBytes=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$' diff --git a/hertzbeat-manager/src/main/resources/define/app-hbase_regionserver.yml b/hertzbeat-manager/src/main/resources/define/app-hbase_regionserver.yml new file mode 100644 index 0000000..0de57a3 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-hbase_regionserver.yml @@ -0,0 +1,606 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: hbase_regionserver +# The monitoring i18n name +name: + zh-CN: Apache Hbase RegionServer + en-US: Apache Hbase RegionServer + ja-JP: Apache Hbase RegionServer +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Hbase 数据库 RegionServer 节点监控指标进行监控。<br>您可以点击 “<i>新建 Apache Hbase RegionServer</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors the RegionServer node monitoring indicators of the Hbase database. <br>You can click "<i>New Apache Hbase RegionServer</i>" to configure, or select "<i>More Actions</i>" to import an existing configuration. + zh-TW: Hertzbeat 對 Hbase 數據庫 RegionServer 节點監控指標進行監控。<br>您可以點擊 “<i>新建 Apache Hbase RegionServer</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は HbaseデータベースのRegionServerノードの一般的なメトリクスを監視します。<br>「<i>新規 Apache Hbase RegionServer</i>」をクリックしてパラメタを設定した後、新規することができます。 + +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/hbase_regionserver/ + en-US: https://hertzbeat.apache.org/docs/help/hbase_regionserver/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 16030 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启用HTTPS + en-US: HTTPS + ja-JP: HTTPS + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true +# collect metrics config list +metrics: + # metrics - Server + - name: Server + i18n: + zh-CN: RegionServer 服务信息 + en-US: RegionServer Service Info + ja-JP: RegionServerサービス情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: regionCount + type: 0 + i18n: + zh-CN: Region数量 + en-US: regionCount + ja-JP: Region数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: readRequestCount + type: 0 + i18n: + zh-CN: 重启集群后的读请求数量 + en-US: readRequestCount + ja-JP: 読み取りリクエスト数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: writeRequestCount + type: 0 + i18n: + zh-CN: 重启集群后的写请求数量 + en-US: writeRequestCount + ja-JP: 書き込みリクエスト数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: averageRegionSize + type: 0 + i18n: + zh-CN: 平均Region大小 + en-US: averageRegionSize + ja-JP: Regionの平均サイズ + unit: 'MB' + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: totalRequestCount + type: 0 + i18n: + zh-CN: 全部请求数量 + en-US: totalRequestCount + ja-JP: リクエスト総数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ScanTime_num_ops + type: 0 + i18n: + zh-CN: Scan 请求总量 + en-US: ScanTime_num_ops + ja-JP: Scan操作回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: Append_num_ops + type: 0 + i18n: + zh-CN: Append 请求量 + en-US: Append_num_ops + ja-JP: Append操作回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: Increment_num_ops + type: 0 + i18n: + zh-CN: Increment请求量 + en-US: Increment_num_ops + ja-JP: Increment操作回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: Get_num_ops + type: 0 + i18n: + zh-CN: Get 请求量 + en-US: Get_num_ops + ja-JP: Get操作回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: Delete_num_ops + type: 0 + i18n: + zh-CN: Delete 请求量 + en-US: Delete_num_ops + ja-JP: Delete操作回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: Put_num_ops + type: 0 + i18n: + zh-CN: Put 请求量 + en-US: Put_num_ops + ja-JP: Put操作回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ScanTime_mean + type: 0 + i18n: + zh-CN: 平均 Scan 请求时间 + en-US: ScanTime_mean + ja-JP: Scan操作の平均時間 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ScanTime_min + type: 0 + i18n: + zh-CN: 最小 Scan 请求时间 + en-US: ScanTime_min + ja-JP: Scan操作の最小時間 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ScanTime_max + type: 0 + i18n: + zh-CN: 最大 Scan 请求时间 + en-US: ScanTime_max + ja-JP: Scan操作の最大時間 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ScanSize_mean + type: 0 + i18n: + zh-CN: 平均 Scan 请求大小 + en-US: ScanSize_mean + ja-JP: Scan操作の平均サイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ScanSize_min + type: 0 + i18n: + zh-CN: 最小 Scan 请求大小 + en-US: ScanSize_min + ja-JP: Scan操作の最小サイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ScanSize_max + type: 0 + i18n: + zh-CN: 最大 Scan 请求大小 + en-US: ScanSize_max + ja-JP: Scan操作の最大サイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: slowPutCount + type: 0 + i18n: + zh-CN: 慢操作次数/Put + en-US: slowPutCount + ja-JP: スローPut操作回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: slowGetCount + type: 0 + i18n: + zh-CN: 慢操作次数/Get + en-US: slowGetCount + ja-JP: スローGet操作回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: slowAppendCount + type: 0 + i18n: + zh-CN: 慢操作次数/Append + en-US: slowAppendCount + ja-JP: スローAppend操作回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: slowIncrementCount + type: 0 + i18n: + zh-CN: 慢操作次数/Increment + en-US: slowIncrementCount + ja-JP: スローIncrement操作回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: slowDeleteCount + type: 0 + i18n: + zh-CN: 慢操作次数/Delete + en-US: slowDeleteCount + ja-JP: スローDelete操作回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: blockCacheSize + type: 0 + unit: 'MB' + i18n: + zh-CN: 缓存块内存占用大小 + en-US: blockCacheSize + ja-JP: ブロックキャッシュのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: blockCacheCount + type: 0 + i18n: + zh-CN: 缓存块数量_Block Cache 中的 Block 数量 + en-US: blockCacheCount + ja-JP: ブロックキャッシュ数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: blockCacheExpressHitPercent + type: 0 + i18n: + zh-CN: 读缓存命中率 + en-US: blockCacheExpressHitPercent + ja-JP: ブロックキャッシュ命中率 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: memStoreSize + type: 0 + i18n: + zh-CN: Memstore 大小 + en-US: memStoreSize + ja-JP: Memstoreサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: FlushTime_num_ops + type: 0 + i18n: + zh-CN: RS写磁盘次数/MemStore Flush 写磁盘次数 + en-US: FlushTime_num_ops + ja-JP: MemStore Flush操作回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: flushQueueLength + type: 0 + i18n: + zh-CN: Region Flush 队列长度 + en-US: flushQueueLength + ja-JP: Region Flushキューの長さ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: flushedCellsSize + type: 0 + unit: 'MB' + i18n: + zh-CN: flush到磁盘大小 + en-US: flushedCellsSize + ja-JP: flushedサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: storeCount + type: 0 + i18n: + zh-CN: Store 个数 + en-US: storeCount + ja-JP: Store数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: storeFileCount + type: 0 + i18n: + zh-CN: Storefile 个数 + en-US: storeFileCount + ja-JP: Storeファイル数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: storeFileSize + type: 0 + unit: 'MB' + i18n: + zh-CN: Storefile 大小 + en-US: storeFileSize + ja-JP: Storeファイルサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: compactionQueueLength + type: 0 + i18n: + zh-CN: Compaction 队列长度 + en-US: compactionQueueLength + ja-JP: Compactionキューの長さ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: percentFilesLocal + type: 0 + i18n: + zh-CN: Region 的 HFile 位于本地 HDFS data node的比例 + en-US: percentFilesLocal + ja-JP: Regionのローカルファイルのパーセント + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: percentFilesLocalSecondaryRegions + type: 0 + i18n: + zh-CN: Region 副本的 HFile 位于本地 HDFS data node的比例 + en-US: percentFilesLocalSecondaryRegions + ja-JP: Secondary Regionのローカルファイルのパーセント + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hlogFileCount + type: 0 + i18n: + zh-CN: WAL 文件数量 + en-US: hlogFileCount + ja-JP: WALファイル数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hlogFileSize + type: 0 + i18n: + zh-CN: WAL 文件大小 + en-US: hlogFileSize + ja-JP: WALファイルサイズ + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.regionCount + - $.readRequestCount + - $.writeRequestCount + - $.averageRegionSize + - $.totalRequestCount + - $.ScanTime_num_ops + - $.Append_num_ops + - $.Increment_num_ops + - $.Get_num_ops + - $.Delete_num_ops + - $.Put_num_ops + - $.ScanTime_mean + - $.ScanTime_min + - $.ScanTime_max + - $.ScanSize_mean + - $.ScanSize_min + - $.ScanSize_max + - $.slowPutCount + - $.slowGetCount + - $.slowAppendCount + - $.slowIncrementCount + - $.slowDeleteCount + - $.blockCacheSize + - $.blockCacheCount + - $.blockCacheExpressHitPercent + - $.memStoreSize + - $.FlushTime_num_ops + - $.flushQueueLength + - $.flushedCellsSize + - $.storeCount + - $.storeFileCount + - $.storeFileSize + - $.compactionQueueLength + - $.percentFilesLocal + - $.percentFilesLocalSecondaryRegions + - $.hlogFileCount + - $.hlogFileSize + calculates: + - regionCount=$.regionCount + - readRequestCount=$.readRequestCount + - writeRequestCount=$.writeRequestCount + - averageRegionSize=$.averageRegionSize + - totalRequestCount=$.totalRequestCount + - ScanTime_num_ops=$.ScanTime_num_ops + - Append_num_ops=$.Append_num_ops + - Increment_num_ops=$.Increment_num_ops + - Get_num_ops=$.Get_num_ops + - Delete_num_ops=$.Delete_num_ops + - Put_num_ops=$.Put_num_ops + - ScanTime_mean=$.ScanTime_mean + - ScanTime_min=$.ScanTime_min + - ScanTime_max=$.ScanTime_max + - ScanSize_mean=$.ScanSize_mean + - ScanSize_min=$.ScanSize_min + - ScanSize_max=$.ScanSize_max + - slowPutCount=$.slowPutCount + - slowGetCount=$.slowGetCount + - slowAppendCount=$.slowAppendCount + - slowIncrementCount=$.slowIncrementCount + - slowDeleteCount=$.slowDeleteCount + - blockCacheSize=$.blockCacheSize + - blockCacheCount=$.blockCacheCount + - blockCacheExpressHitPercent=$.blockCacheExpressHitPercent + - memStoreSize=$.memStoreSize + - FlushTime_num_ops=$.FlushTime_num_ops + - flushQueueLength=$.flushQueueLength + - flushedCellsSize=$.flushedCellsSize + - storeCount=$.storeCount + - storeFileCount=$.storeFileCount + - storeFileSize=$.storeFileSize + - compactionQueueLength=$.compactionQueueLength + - percentFilesLocal=$.percentFilesLocal + - percentFilesLocalSecondaryRegions=$.percentFilesLocalSecondaryRegions + - hlogFileCount=$.hlogFileCount + - hlogFileSize=$.hlogFileSize + units: + - averageRegionSize=B->MB + - blockCacheSize=B->MB + - storeFileSize=B->MB + - flushedCellsSize=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name == "Hadoop:service=HBase,name=RegionServer,sub=Server")]' + # metrics - IPC + - name: IPC + i18n: + zh-CN: RegionServer IPC 信息 + en-US: RegionServer IPC Info + ja-JP: RegionServer IPC 情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: numActiveHandler + type: 0 + i18n: + zh-CN: RPC句柄数 + en-US: numActiveHandler + ja-JP: RPCハンドル数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: NotServingRegionException + type: 0 + i18n: + zh-CN: NotServingRegionException 异常数量 + en-US: NotServingRegionException + ja-JP: NotServingRegionException + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: RegionMovedException + type: 0 + i18n: + zh-CN: RegionMovedException异常数量 + en-US: RegionMovedException + ja-JP: RegionMovedException + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: RegionTooBusyException + type: 0 + i18n: + zh-CN: RegionTooBusyException异常数量 + en-US: RegionTooBusyException + ja-JP: RegionTooBusyException + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.numActiveHandler + - $.['exceptions.NotServingRegionException'] + - $.['exceptions.RegionMovedException'] + - $.['exceptions.RegionTooBusyException'] + calculates: + - numActiveHandler=$.numActiveHandler + - NotServingRegionException=$.['exceptions.NotServingRegionException'] + - RegionMovedException=$.['exceptions.RegionMovedException'] + - RegionTooBusyException=$.['exceptions.RegionTooBusyException'] + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name == "Hadoop:service=HBase,name=RegionServer,sub=IPC")]' + # metrics - JVM + - name: JVM + i18n: + zh-CN: RegionServer JVM 信息 + en-US: RegionServer JVM Info + ja-JP: RegionServer Java仮想マシン情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemNonHeapUsedM + type: 0 + unit: 'MB' + i18n: + zh-CN: 进程使用的非堆内存大小 + en-US: MemNonHeapUsedM + ja-JP: 使用したノンヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemNonHeapCommittedM + type: 0 + unit: 'MB' + i18n: + zh-CN: 进程 commit 的非堆内存大小 + en-US: MemNonHeapCommittedM + ja-JP: コミットされたノンヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemHeapUsedM + type: 0 + unit: 'MB' + i18n: + zh-CN: 进程使用的堆内存大小 + en-US: MemHeapUsedM + ja-JP: 使用したヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemHeapCommittedM + type: 0 + unit: 'MB' + i18n: + zh-CN: 进程 commit 的堆内存大小 + en-US: MemHeapCommittedM + ja-JP: コミットされたヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemHeapMaxM + type: 0 + unit: 'MB' + i18n: + zh-CN: 进程最大的堆内存大小 + en-US: MemHeapMaxM + ja-JP: ヒープメモリサイズの最大値 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemMaxM + type: 0 + unit: 'MB' + i18n: + zh-CN: 进程最大内存大小 + en-US: MemMaxM + ja-JP: メモリサイズの最大値 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: GcCount + type: 0 + i18n: + zh-CN: Young GC次数 + en-US: GcCount + ja-JP: GC回数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.MemNonHeapUsedM + - $.MemNonHeapCommittedM + - $.MemHeapUsedM + - $.MemHeapCommittedM + - $.MemHeapMaxM + - $.MemMaxM + - $.GcCount + calculates: + - MemNonHeapUsedM=$.MemNonHeapUsedM + - MemNonHeapCommittedM=$.MemNonHeapCommittedM + - MemHeapUsedM=$.MemHeapUsedM + - MemHeapCommittedM=$.MemHeapCommittedM + - MemHeapMaxM=$.MemHeapMaxM + - MemMaxM=$.MemMaxM + - GcCount=$.GcCount + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name == "Hadoop:service=HBase,name=JvmMetrics")]' diff --git a/hertzbeat-manager/src/main/resources/define/app-hdfs_datanode.yml b/hertzbeat-manager/src/main/resources/define/app-hdfs_datanode.yml new file mode 100644 index 0000000..363b8a0 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-hdfs_datanode.yml @@ -0,0 +1,267 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: hdfs_datanode +# The monitoring i18n name +name: + zh-CN: Apache HDFS DataNode + en-US: Apache HDFS DataNode + ja-JP: Apache HDFS DataNode +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 HDFS DataNode 节点监控指标进行监控。<br>您可以点击 “<i>新建 Apache HDFS DataNode</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors the HDFS DataNode metrics. <br>You can click "<i>New Apache HDFS DataNode</i>" to configure, or select "<i>More Actions</i>" to import an existing configuration. + zh-TW: Hertzbeat 對 HDFS DataNode 節點監控指標進行監控。<br>您可以點擊 “<i>新建 Apache HDFS DataNode</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は HDFS DataNodeの一般的なメトリクスを監視します。<br>「<i>新規 Apache HDFS DataNode</i>」をクリックしてパラメタを設定した後、新規することができます。 + +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/hdfs_datanode/ + en-US: https://hertzbeat.apache.org/docs/help/hdfs_datanode/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 50075 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - Server + - name: FSDatasetState + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: DfsUsed + type: 0 + unit: 'GB' + i18n: + zh-CN: DataNode HDFS使用量 + en-US: DfsUsed + ja-JP: 使用したHDFS容量 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: Remaining + type: 0 + unit: 'GB' + i18n: + zh-CN: DataNode HDFS剩余空间 + en-US: Remaining + ja-JP: 使用可能のHDFS容量 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: Capacity + type: 0 + unit: 'GB' + i18n: + zh-CN: DataNode HDFS空间总量 + en-US: Capacity + ja-JP: HDFS容量合計 + units: + - DfsUsed=B->GB + - Remaining=B->GB + - Capacity=B->GB + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.DfsUsed + - $.Remaining + - $.Capacity + calculates: + - DfsUsed=$.DfsUsed + - Remaining=$.Remaining + - Capacity=$.Capacity + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name == "Hadoop:service=DataNode,name=FSDatasetState")]' + - name: JvmMetrics + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemNonHeapUsedM + type: 0 + i18n: + zh-CN: JVM 当前已经使用的 NonHeapMemory 的大小 + en-US: MemNonHeapUsedM + ja-JP: 使用したノンヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemNonHeapCommittedM + type: 0 + i18n: + zh-CN: JVM 配置的 NonHeapCommittedM 的大小 + en-US: MemNonHeapCommittedM + ja-JP: コミットされたノンヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemHeapUsedM + type: 0 + i18n: + zh-CN: JVM 当前已经使用的 HeapMemory 的大小 + en-US: MemHeapUsedM + ja-JP: 使用したヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemHeapCommittedM + type: 0 + i18n: + zh-CN: JVM HeapMemory 提交大小 + en-US: MemHeapCommittedM + ja-JP: コミットされたヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemHeapMaxM + type: 0 + i18n: + zh-CN: JVM 配置的 HeapMemory 的大小 + en-US: MemHeapMaxM + ja-JP: 配置のヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemMaxM + type: 0 + i18n: + zh-CN: JVM 运行时可以使用的最大内存大小 + en-US: MemMaxM + ja-JP: 最大のヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ThreadsRunnable + type: 0 + i18n: + zh-CN: 处于 RUNNABLE 状态的线程数量 + en-US: ThreadsRunnable + ja-JP: 実行中のスレッド数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ThreadsBlocked + type: 0 + i18n: + zh-CN: 处于 BLOCKED 状态的线程数量 + en-US: ThreadsBlocked + ja-JP: ブロックされたスレッド数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ThreadsWaiting + type: 0 + i18n: + zh-CN: 处于 WAITING 状态的线程数量 + en-US: ThreadsWaiting + ja-JP: 待機中のスレッド数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ThreadsTimedWaiting + type: 0 + i18n: + zh-CN: 处于 TIMED WAITING 状态的线程数量 + en-US: ThreadsTimedWaiting + ja-JP: 時間指定の待機中のスレッド数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.MemNonHeapUsedM + - $.MemNonHeapCommittedM + - $.MemHeapUsedM + - $.MemHeapCommittedM + - $.MemHeapMaxM + - $.MemMaxM + - $.ThreadsRunnable + - $.ThreadsBlocked + - $.ThreadsWaiting + - $.ThreadsTimedWaiting + calculates: + - MemNonHeapUsedM=$.MemNonHeapUsedM + - MemNonHeapCommittedM=$.MemNonHeapCommittedM + - MemHeapUsedM=$.MemHeapUsedM + - MemHeapCommittedM=$.MemHeapCommittedM + - MemHeapMaxM=$.MemHeapMaxM + - MemMaxM=$.MemMaxM + - ThreadsRunnable=$.ThreadsRunnable + - ThreadsBlocked=$.ThreadsBlocked + - ThreadsWaiting=$.ThreadsWaiting + - ThreadsTimedWaiting=$.ThreadsTimedWaiting + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name == "Hadoop:service=DataNode,name=JvmMetrics")]' + - name: runtime + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: StartTime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: StartTime + ja-JP: 起動時間 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.beans[?(@.name == "java.lang:type=Runtime")].StartTime + calculates: + - StartTime=$.beans[?(@.name == "java.lang:type=Runtime")].StartTime + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$' diff --git a/hertzbeat-manager/src/main/resources/define/app-hdfs_namenode.yml b/hertzbeat-manager/src/main/resources/define/app-hdfs_namenode.yml new file mode 100644 index 0000000..de15ce9 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-hdfs_namenode.yml @@ -0,0 +1,561 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: hdfs_namenode +# The monitoring i18n name +name: + zh-CN: Apache HDFS NameNode + en-US: Apache HDFS NameNode + ja-JP: Apache HDFS NameNode +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 HDFS NameNode 节点监控指标进行监控。<br>您可以点击 “<i>新建 Apache HDFS NameNode</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors the HDFS NameNode metrics. <br>You can click "<i>New Apache HDFS NameNode</i>" to configure, or select "<i>More Actions</i>" to import an existing configuration. + zh-TW: Hertzbeat 對 HDFS NameNode 節點監控指標進行監控。<br>您可以點擊 “<i>新建 Apache HDFS NameNode</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は HDFS NameNodeの一般的なメトリクスを監視します。<br>「<i>新規 Apache HDFS NameNode</i>」をクリックしてパラメタを設定した後、新規することができます。 + +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/hdfs_namenode/ + en-US: https://hertzbeat.apache.org/docs/help/hdfs_namenode/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 50070 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - Server + - name: FSNamesystem + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: CapacityTotal + type: 0 + i18n: + zh-CN: 集群存储总容量 + en-US: CapacityTotal + ja-JP: 容量合計 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: CapacityTotalGB + type: 0 + unit: 'GB' + i18n: + zh-CN: 集群存储总容量 + en-US: CapacityTotalGB + ja-JP: 容量合計(GB) + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: CapacityUsed + type: 0 + i18n: + zh-CN: 集群存储已使用容量 + en-US: CapacityUsed + ja-JP: 使用した容量 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: CapacityUsedGB + type: 0 + i18n: + zh-CN: 集群存储已使用容量 + unit: 'GB' + en-US: CapacityUsedGB + ja-JP: 使用した容量(GB) + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: CapacityRemaining + type: 0 + i18n: + zh-CN: 集群存储剩余容量 + en-US: CapacityRemaining + ja-JP: 使用可能の容量 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: CapacityRemainingGB + type: 0 + unit: 'GB' + i18n: + zh-CN: 集群存储剩余容量 + en-US: CapacityRemainingGB + ja-JP: 使用可能の容量(GB) + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: CapacityUsedNonDFS + type: 0 + i18n: + zh-CN: 集群非 HDFS 使用容量 + en-US: CapacityUsedNonDFS + ja-JP: 使用したノンHDFS容量 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: TotalLoad + type: 0 + i18n: + zh-CN: 整个集群的客户端连接数 + en-US: TotalLoad + ja-JP: クライアントとの接続総数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: FilesTotal + type: 0 + i18n: + zh-CN: 集群文件总数量 + en-US: FilesTotal + ja-JP: ファイル総数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: BlocksTotal + type: 0 + i18n: + zh-CN: 总 BLOCK 数量 + en-US: BlocksTotal + ja-JP: ブロック総数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: PendingReplicationBlocks + type: 0 + i18n: + zh-CN: 等待被备份的块数量 + en-US: PendingReplicationBlocks + ja-JP: 保留中のレプリケーションのブロック数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: UnderReplicatedBlocks + type: 0 + i18n: + zh-CN: 副本数不够的块数量 + en-US: UnderReplicatedBlocks + ja-JP: レプリケート中のブロック数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: CorruptBlocks + type: 0 + i18n: + zh-CN: 坏块数量 + en-US: CorruptBlocks + ja-JP: 破損ブロック数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ScheduledReplicationBlocks + type: 0 + i18n: + zh-CN: 安排要备份的块数量 + en-US: ScheduledReplicationBlocks + ja-JP: スケジュールされたレプリケーションのブロック数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: PendingDeletionBlocks + type: 0 + i18n: + zh-CN: 等待被删除的块数量 + en-US: PendingDeletionBlocks + ja-JP: 保留中の削除ブロック数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ExcessBlocks + type: 0 + i18n: + zh-CN: 多余的块数量 + en-US: ExcessBlocks + ja-JP: 余剰ブロック数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: PostponedMisreplicatedBlocks + type: 0 + i18n: + zh-CN: 被推迟处理的异常块数量 + en-US: PostponedMisreplicatedBlocks + ja-JP: 延期の異常ブロック数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: NumLiveDataNodes + type: 0 + i18n: + zh-CN: 活的数据节点数量 + en-US: NumLiveDataNodes + ja-JP: ライブのDATANODE数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: NumDeadDataNodes + type: 0 + i18n: + zh-CN: 已经标记为 Dead 状态的数据节点数量 + en-US: NumDeadDataNodes + ja-JP: デッドのDATANODE数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: NumDecomLiveDataNodes + type: 0 + i18n: + zh-CN: 下线且 Live 的节点数量 + en-US: NumDecomLiveDataNodes + ja-JP: オフラインとライブのDATANODE数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: NumDecomDeadDataNodes + type: 0 + i18n: + zh-CN: 下线且 Dead 的节点数量 + en-US: NumDecomDeadDataNodes + ja-JP: オフラインとデッドのDATANODE数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: NumDecommissioningDataNodes + type: 0 + i18n: + zh-CN: 正在下线的节点数量 + en-US: NumDecommissioningDataNodes + ja-JP: オフライン中のDATANODE数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: TransactionsSinceLastCheckpoint + type: 0 + i18n: + zh-CN: 从上次Checkpoint之后的事务数量 + en-US: TransactionsSinceLastCheckpoint + ja-JP: 前回のチェックポイントからのトランザクション数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: LastCheckpointTime + type: 0 + i18n: + zh-CN: 上一次Checkpoint时间 + en-US: LastCheckpointTime + ja-JP: 最終チェックポイント時刻 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: PendingDataNodeMessageCount + type: 0 + i18n: + zh-CN: DATANODE 的请求被 QUEUE 在 standby namenode 中的个数 + en-US: PendingDataNodeMessageCount + ja-JP: DATANODEで保留中のメッセージ数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.CapacityTotal + - $.CapacityTotalGB + - $.CapacityUsed + - $.CapacityUsedGB + - $.CapacityRemaining + - $.CapacityRemainingGB + - $.CapacityUsedNonDFS + - $.TotalLoad + - $.FilesTotal + - $.BlocksTotal + - $.PendingReplicationBlocks + - $.UnderReplicatedBlocks + - $.CorruptBlocks + - $.ScheduledReplicationBlocks + - $.PendingDeletionBlocks + - $.ExcessBlocks + - $.PostponedMisreplicatedBlocks + - $.NumLiveDataNodes + - $.NumDeadDataNodes + - $.NumDecomLiveDataNodes + - $.NumDecomDeadDataNodes + - $.NumDecommissioningDataNodes + - $.TransactionsSinceLastCheckpoint + - $.LastCheckpointTime + - $.PendingDataNodeMessageCount + calculates: + - CapacityTotal=$.CapacityTotal + - CapacityTotalGB=$.CapacityTotalGB + - CapacityUsed=$.CapacityUsed + - CapacityUsedGB=$.CapacityUsedGB + - CapacityRemaining=$.CapacityRemaining + - CapacityRemainingGB=$.CapacityRemainingGB + - CapacityUsedNonDFS=$.CapacityUsedNonDFS + - TotalLoad=$.TotalLoad + - FilesTotal=$.FilesTotal + - BlocksTotal=$.BlocksTotal + - PendingReplicationBlocks=$.PendingReplicationBlocks + - UnderReplicatedBlocks=$.UnderReplicatedBlocks + - CorruptBlocks=$.CorruptBlocks + - ScheduledReplicationBlocks=$.ScheduledReplicationBlocks + - PendingDeletionBlocks=$.PendingDeletionBlocks + - ExcessBlocks=$.ExcessBlocks + - PostponedMisreplicatedBlocks=$.PostponedMisreplicatedBlocks + - NumLiveDataNodes=$.NumLiveDataNodes + - NumDeadDataNodes=$.NumDeadDataNodes + - NumDecomLiveDataNodes=$.NumDecomLiveDataNodes + - NumDecomDeadDataNodes=$.NumDecomDeadDataNodes + - NumDecommissioningDataNodes=$.NumDecommissioningDataNodes + - TransactionsSinceLastCheckpoint=$.TransactionsSinceLastCheckpoint + - LastCheckpointTime=$.LastCheckpointTime + - PendingDataNodeMessageCount=$.PendingDataNodeMessageCount + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name == "Hadoop:service=NameNode,name=FSNamesystem")]' + - name: RPC + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ReceivedBytes + type: 0 + i18n: + zh-CN: 接收数据速率 + en-US: ReceivedBytes + ja-JP: 受信されたバイト + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: SentBytes + type: 0 + i18n: + zh-CN: 发送数据速率 + en-US: SentBytes + ja-JP: 転送されたバイト + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: RpcQueueTimeNumOps + type: 0 + i18n: + zh-CN: RPC 调用速率 + en-US: RpcQueueTimeNumOps + ja-JP: RPCスピード + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.ReceivedBytes + - $.SentBytes + - $.RpcQueueTimeNumOps + calculates: + - ReceivedBytes=$.ReceivedBytes + - SentBytes=$.SentBytes + - RpcQueueTimeNumOps=$.RpcQueueTimeNumOps + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name =~ /Hadoop:service=NameNode,name=RpcActivityForPort80\d+/)]' + - name: runtime + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: StartTime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: StartTime + ja-JP: 起動時間 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.beans[?(@.name == "java.lang:type=Runtime")].StartTime + calculates: + - StartTime=$.beans[?(@.name == "java.lang:type=Runtime")].StartTime + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$' + + - name: JvmMetric + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemNonHeapUsedM + type: 0 + i18n: + zh-CN: JVM 当前已经使用的 NonHeapMemory 的大小 + en-US: MemNonHeapUsedM + ja-JP: 使用したノンヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemNonHeapCommittedM + type: 0 + i18n: + zh-CN: JVM 配置的 NonHeapCommittedM 的大小 + en-US: MemNonHeapCommittedM + ja-JP: コミットされたノンヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemHeapUsedM + type: 0 + i18n: + zh-CN: JVM 当前已经使用的 HeapMemory 的大小 + en-US: MemHeapUsedM + ja-JP: 使用したヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemHeapCommittedM + type: 0 + i18n: + zh-CN: JVM HeapMemory 提交大小 + en-US: MemHeapCommittedM + ja-JP: コミットされたヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemHeapMaxM + type: 0 + i18n: + zh-CN: JVM 配置的 HeapMemory 的大小 + en-US: MemHeapMaxM + ja-JP: 配置のヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemMaxM + type: 0 + i18n: + zh-CN: JVM 运行时可以使用的最大内存大小 + en-US: MemMaxM + ja-JP: 最大のヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: GcCountParNew + type: 0 + i18n: + zh-CN: 新生代GC次数 + en-US: GcCountParNew + ja-JP: 若い領域GC回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: GcTimeMillisParNew + type: 0 + i18n: + zh-CN: 新生代GC消耗时间 + en-US: GcTimeMillisParNew + ja-JP: 若い領域GC時間 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: GcCountConcurrentMarkSweep + type: 0 + i18n: + zh-CN: 老年代GC次数 + en-US: GcCountConcurrentMarkSweep + ja-JP: 古い領域GC回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: GcTimeMillisConcurrentMarkSweep + type: 0 + i18n: + zh-CN: 老年代GC消耗时间 + en-US: GcTimeMillisConcurrentMarkSweep + ja-JP: 古い領域GC時間 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: GcCount + type: 0 + i18n: + zh-CN: GC次数 + en-US: GcCount + ja-JP: GC回数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: GcTimeMillis + type: 0 + i18n: + zh-CN: GC消耗时间 + en-US: GcTimeMillis + ja-JP: GC時間 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ThreadsRunnable + type: 0 + i18n: + zh-CN: 处于 RUNNABLE 状态的线程数量 + en-US: ThreadsRunnable + ja-JP: 実行中のスレッド数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ThreadsBlocked + type: 0 + i18n: + zh-CN: 处于 BLOCKED 状态的线程数量 + en-US: ThreadsBlocked + ja-JP: ブロックされたスレッド数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ThreadsWaiting + type: 0 + i18n: + zh-CN: 处于 WAITING 状态的线程数量 + en-US: ThreadsWaiting + ja-JP: 待機中のスレッド数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ThreadsTimedWaiting + type: 0 + i18n: + zh-CN: 处于 TIMED WAITING 状态的线程数量 + en-US: ThreadsTimedWaiting + ja-JP: 時間指定の待機中のスレッド数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.MemNonHeapUsedM + - $.MemNonHeapCommittedM + - $.MemHeapUsedM + - $.MemHeapCommittedM + - $.MemHeapMaxM + - $.MemMaxM + - $.GcCountParNew + - $.GcTimeMillisParNew + - $.GcCountConcurrentMarkSweep + - $.GcTimeMillisConcurrentMarkSweep + - $.GcCount + - $.GcTimeMillis + - $.ThreadsRunnable + - $.ThreadsBlocked + - $.ThreadsWaiting + - $.ThreadsTimedWaiting + calculates: + - MemNonHeapUsedM=$.MemNonHeapUsedM + - MemNonHeapCommittedM=$.MemNonHeapCommittedM + - MemHeapUsedM=$.MemHeapUsedM + - MemHeapCommittedM=$.MemHeapCommittedM + - MemHeapMaxM=$.MemHeapMaxM + - MemMaxM=$.MemMaxM + - GcCountParNew=$.GcCountParNew + - GcTimeMillisParNew=$.GcTimeMillisParNew + - GcCountConcurrentMarkSweep=$.GcCountConcurrentMarkSweep + - GcTimeMillisConcurrentMarkSweep=$.GcTimeMillisConcurrentMarkSweep + - GcCount=$.GcCount + - GcTimeMillis=$.GcTimeMillis + - ThreadsRunnable=$.ThreadsRunnable + - ThreadsBlocked=$.ThreadsBlocked + - ThreadsWaiting=$.ThreadsWaiting + - ThreadsTimedWaiting=$.ThreadsTimedWaiting + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name == "Hadoop:service=NameNode,name=JvmMetrics")]' diff --git a/hertzbeat-manager/src/main/resources/define/app-hertzbeat.yml b/hertzbeat-manager/src/main/resources/define/app-hertzbeat.yml new file mode 100644 index 0000000..c98e13e --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-hertzbeat.yml @@ -0,0 +1,376 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: custom +# The monitoring type eg: linux windows tomcat mysql aws... +app: hertzbeat +name: + zh-CN: HertzBeat + en-US: HertzBeat + ja-JP: HertzBeat +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Hertzbeat 监控系统的通用指标进行测量监控。<br>您可以点击 “<i>新建 HertzBeat监控系统</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors HertzBeat Monitor through general performance metric. You could click the "<i>New HertzBeat Monitor</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat對Hertzbeat監控系統的通用指標進行量測監控。<br>您可以點擊“<i>新建HertzBeat監控系統</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は Hertzbeatの一般的なメトリクスを監視します。<br>「<i>新規 Apache Hertzbeat</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/hertzbeat + en-US: https://hertzbeat.apache.org/docs/help/hertzbeat +params: + - field: host + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + required: true + defaultValue: 1157 + - field: ssl + name: + zh-CN: 启用HTTPS + en-US: HTTPS + ja-JP: HTTPS + type: boolean + required: true + - field: timeout + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + type: number + required: false + hide: true + defaultValue: 6000 + - field: authType + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + type: radio + required: false + hide: true + options: + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + hide: true + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + hide: true +metrics: + # the first metrics summary + # attention: Built-in monitoring metrics contains (responseTime - Response time) + - name: summary + i18n: + zh-CN: 概要信息 + en-US: Summary + ja-JP: 概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # metrics content contains field-metric name, type-metric type:0-number,1-string, instance-if is metrics, unit-metric unit('%','ms','MB') + - field: app + type: 1 + label: true + i18n: + zh-CN: 应用 + en-US: App + ja-JP: 応用 + - field: category + type: 1 + i18n: + zh-CN: 类别 + en-US: Category + ja-JP: カテゴリ + - field: status + type: 0 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + - field: Size + type: 0 + i18n: + zh-CN: 数量 + en-US: Size + ja-JP: サイズ + - field: availableSize + type: 0 + i18n: + zh-CN: 可用数量 + en-US: Available Size + ja-JP: 利用可能なサイズ + aliasFields: + - $.app + - $.category + - $.status + - $.size + - $.availableSize + calculates: + - app=$.app + - category=$.category + - status=$.status + - Size=$.size + - availableSize=$.availableSize + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, we use HTTP protocol here + protocol: http + # the config content when protocol is http + http: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url, we don't need to enter a parameter here, just set the fixed value to /api/summary + url: /api/summary + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH, default fixed value is GET + method: GET + # if enabled https, default value is false + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, we use jsonpath to parse response data here + parseType: jsonPath + parseScript: '$.data.apps.*' + + - name: inner_queue + i18n: + zh-CN: 内部队列 + en-US: Inner Queue + ja-JP: 内部キュー + priority: 1 + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: alertDataQueue + type: 0 + i18n: + zh-CN: 变更数据队列 + en-US: Data Change Queue + ja-JP: データ変更キュー + - field: metricsDataToAlertQueue + type: 0 + i18n: + zh-CN: 指标数据到变更队列 + en-US: Metrics Data to Change Queue + ja-JP: データ変更キュー + - field: metricsDataToPersistentStorageQueue + type: 0 + i18n: + zh-CN: 指标数据到持久化队列 + en-US: Metrics Data to Persistent Queue + ja-JP: 永続キュー + - field: metricsDataToMemoryStorageQueue + type: 0 + i18n: + zh-CN: 指标数据到内存存储队列 + en-US: Metrics Data to Memory Storage Queue + ja-JP: メモリストレージキュー + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /api/metrics + timeout: ^_^timeout^_^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.data' + + - name: thread_state + i18n: + zh-CN: 线程状态 + en-US: Thread State + ja-JP: スレッド状態 + visible: false + priority: 1 + fields: + - field: state + type: 1 + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /actuator/metrics/jvm.threads.states + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.availableTags[?(@.tag == "state")].values[*]' + + - name: threads + i18n: + zh-CN: 线程 + en-US: Threads + ja-JP: スレッド + priority: 3 + fields: + - field: state + type: 1 + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + - field: number + type: 0 + i18n: + zh-CN: 数量 + en-US: Number + ja-JP: 総数 + aliasFields: + - $.measurements[?(@.statistic == "VALUE")].value + calculates: + - state='^o^state^o^' + - number=$.measurements[?(@.statistic == "VALUE")].value + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /actuator/metrics/jvm.threads.states?tag=state:^o^state^o^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$' + + - name: space_name + i18n: + zh-CN: 空间名称 + en-US: Space Name + ja-JP: スペース名 + visible: false + priority: 4 + fields: + - field: id + type: 1 + i18n: + zh-CN: ID + en-US: ID + ja-JP: ID + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /actuator/metrics/jvm.memory.used + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.availableTags[?(@.tag == "id")].values[*]' + + - name: memory_used + i18n: + zh-CN: 内存使用 + en-US: Memory Used + ja-JP: 使用したメモリ + priority: 5 + fields: + - field: space + type: 1 + i18n: + zh-CN: 空间 + en-US: Space + ja-JP: スペース + - field: mem_used + type: 0 + unit: MB + i18n: + zh-CN: 已使用内存 + en-US: Used Memory + ja-JP: 使用したメモリ + aliasFields: + - $.measurements[?(@.statistic == "VALUE")].value + calculates: + - space="^o^id^o^" + - mem_used=$.measurements[?(@.statistic == "VALUE")].value + units: + - mem_used=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /actuator/metrics/jvm.memory.used?tag=id:^o^id^o^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$' diff --git a/hertzbeat-manager/src/main/resources/define/app-hertzbeat_token.yml b/hertzbeat-manager/src/main/resources/define/app-hertzbeat_token.yml new file mode 100644 index 0000000..d96d9ec --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-hertzbeat_token.yml @@ -0,0 +1,387 @@ +# 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. + + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: custom +# The monitoring type eg: linux windows tomcat mysql aws... +app: hertzbeat_token +# The monitoring i18n name +name: + zh-CN: HertzBeat(Token) + en-US: HertzBeat(Token) + ja-JP: HertzBeat(Token) +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 HertzBeat监控(Token)进行测量监控。<br>您可以点击 “<i>新建 HertzBeat监控(Token)</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors HertzBeat Monitor(Token). You could click the "<i>New HertzBeat Monitor(Token)</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat對HertzBeat監控(Token)進行量測監控。<br>您可以點擊“<i>新建HertzBeat監控(Token)</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は Hertzbeat(Token)の一般的なメトリクスを監視します。<br>「<i>新規 Apache Hertzbeat(Token)</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/hertzbeat_token + en-US: https://hertzbeat.apache.org/docs/help/hertzbeat_token +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + required: true + defaultValue: 1157 + placeholder: 'Please input port' + - field: ssl + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL利用 + # type-param field type(boolean mapping the html switch tag) + type: boolean + required: false + - field: contentType + name: + zh-CN: Content-Type + en-US: Content-Type + ja-JP: コンテンツタイプ + type: text + placeholder: 'Request Body Type' + required: false + - field: payload + name: + zh-CN: 请求BODY + en-US: BODY + ja-JP: ボディ + type: textarea + placeholder: 'Available When POST PUT' + required: false +# collect metrics config list +metrics: + # metrics - auth + - name: auth + i18n: + zh-CN: 认证 + en-US: Auth + ja-JP: 認証 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: token + type: 1 + i18n: + zh-CN: 令牌 + en-US: Token + ja-JP: トークン + - field: refreshToken + type: 1 + i18n: + zh-CN: 刷新令牌 + en-US: Refresh Token + ja-JP: リフレッシュトークン + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /api/account/auth/form + # http method: GET POST PUT DELETE PATCH + method: POST + # if enabled https + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + # http request header content + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + # http request params + params: + ^_^params^_^: ^_^params^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$.data' + + + - name: summary + i18n: + zh-CN: 概要信息 + en-US: Summary + ja-JP: 概要 + priority: 1 + fields: + - field: app + type: 1 + label: true + i18n: + zh-CN: 应用 + en-US: App + ja-JP: 応用 + - field: category + type: 1 + i18n: + zh-CN: 类别 + en-US: Category + ja-JP: カテゴリ + - field: status + type: 0 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + - field: Size + type: 0 + i18n: + zh-CN: 数量 + en-US: Size + ja-JP: サイズ + - field: availableSize + type: 0 + i18n: + zh-CN: 可用数量 + en-US: Available Size + ja-JP: 利用可能なサイズ + aliasFields: + - $.app + - $.category + - $.status + - $.size + - $.availableSize + calculates: + - app=$.app + - category=$.category + - status=$.status + - Size=$.size + - availableSize=$.availableSize + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /api/summary + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Bearer Token + # ^o^xxx^o^ ^o^ substitution represents the value of the acquisition metric xxx of the previous priority + bearerTokenToken: ^o^token^o^ + parseType: jsonPath + parseScript: '$.data.apps.*' + + - name: inner_queue + i18n: + zh-CN: 内部队列 + en-US: Inner Queue + ja-JP: 内部キュー + priority: 1 + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: alertDataQueue + type: 0 + i18n: + zh-CN: 变更数据队列 + en-US: Alert Data Queue + ja-JP: アラートデータキュー + - field: metricsDataToAlertQueue + type: 0 + i18n: + zh-CN: 指标数据到变更队列 + en-US: Metrics Data to Alert Queue + ja-JP: アラートキュー + - field: metricsDataToPersistentStorageQueue + type: 0 + i18n: + zh-CN: 指标数据到持久化队列 + en-US: Metrics Data to Persistent Storage Queue + ja-JP: 永続ストレージキュー + - field: metricsDataToMemoryStorageQueue + type: 0 + i18n: + zh-CN: 指标数据到内存存储队列 + en-US: Metrics Data to Memory Storage Queue + ja-JP: メモリストレージキュー + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /api/metrics + timeout: ^_^timeout^_^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Bearer Token + # ^o^xxx^o^ ^o^ substitution represents the value of the acquisition metric xxx of the previous priority + bearerTokenToken: ^o^token^o^ + parseType: jsonPath + parseScript: '$.data' + + - name: thread_state + i18n: + zh-CN: 指标数据到内存存储队列 + en-US: Metrics Data to Memory Storage Queue + ja-JP: メモリストレージキュー + visible: false + priority: 2 + fields: + - field: state + type: 1 + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /actuator/metrics/jvm.threads.states + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Bearer Token + # ^o^xxx^o^ ^o^ substitution represents the value of the acquisition metric xxx of the previous priority + bearerTokenToken: ^o^token^o^ + parseType: jsonPath + parseScript: '$.availableTags[?(@.tag == "state")].values[*]' + + - name: threads + i18n: + zh-CN: 线程 + en-US: Threads + ja-JP: スレッド + priority: 3 + fields: + - field: state + type: 1 + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + - field: number + type: 0 + i18n: + zh-CN: 数量 + en-US: Number + ja-JP: 総数 + aliasFields: + - $.measurements[?(@.statistic == "VALUE")].value + calculates: + - state='^o^state^o^' + - number=$.measurements[?(@.statistic == "VALUE")].value + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /actuator/metrics/jvm.threads.states?tag=state:^o^state^o^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Bearer Token + # ^o^xxx^o^ ^o^ substitution represents the value of the acquisition metric xxx of the previous priority + bearerTokenToken: ^o^token^o^ + parseType: jsonPath + parseScript: '$' + + - name: space_name + i18n: + zh-CN: 空间名称 + en-US: Space Name + ja-JP: スペース名 + visible: false + priority: 4 + fields: + - field: id + type: 1 + i18n: + zh-CN: ID + en-US: ID + ja-JP: ID + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /actuator/metrics/jvm.memory.used + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Bearer Token + # ^o^xxx^o^ ^o^ substitution represents the value of the acquisition metric xxx of the previous priority + bearerTokenToken: ^o^token^o^ + parseType: jsonPath + parseScript: '$.availableTags[?(@.tag == "id")].values[*]' + + - name: memory_used + i18n: + zh-CN: 内存使用 + en-US: Memory Used + ja-JP: 使用したメモリ + priority: 5 + fields: + - field: space + type: 1 + i18n: + zh-CN: 空间 + en-US: Space + ja-JP: スペース + - field: mem_used + type: 0 + unit: MB + i18n: + zh-CN: 内存使用量 + en-US: Memory Used + ja-JP: 使用したメモリ + aliasFields: + - $.measurements[?(@.statistic == "VALUE")].value + calculates: + - space="^o^id^o^" + - mem_used=$.measurements[?(@.statistic == "VALUE")].value + units: + - mem_used=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /actuator/metrics/jvm.memory.used?tag=id:^o^id^o^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Bearer Token + # ^o^xxx^o^ ^o^ substitution represents the value of the acquisition metric xxx of the previous priority + bearerTokenToken: ^o^token^o^ + parseType: jsonPath + parseScript: '$' diff --git a/hertzbeat-manager/src/main/resources/define/app-hikvision_isapi.yml b/hertzbeat-manager/src/main/resources/define/app-hikvision_isapi.yml new file mode 100644 index 0000000..785cd6f --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-hikvision_isapi.yml @@ -0,0 +1,299 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: server +# The monitoring type eg: linux windows tomcat mysql aws... +app: hikvision_isapi +# The monitoring i18n name +name: + zh-CN: 海康威视 ISAPI + en-US: Hikvision ISAPI + ja-JP: Hikvision ISAPI +# The description and help of this monitoring type +help: + zh-CN: 通过ISAPI接口监控海康威视设备状态,获取设备健康数据。 + en-US: Monitor Hikvision devices through ISAPI interface to collect health data. + ja-JP: ISAPIを呼び出して健康データを収集し、Hikvisionデバイスを監視します。 + +# Input params define for monitoring(render web ui by the definition) +params: + - field: host + name: + zh-CN: 主机Host + en-US: Host + ja-JP: ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + required: true + defaultValue: 80 + - field: timeout + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + type: number + range: '[1000,60000]' + required: true + defaultValue: 5000 + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + required: true + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: true + - field: ssl + name: + zh-CN: 启用HTTPS + en-US: SSL + ja-JP: SSL利用 + type: boolean + required: false + defaultValue: false + +# collect metrics config list +metrics: + - name: system_info + i18n: + zh-CN: 系统信息 + en-US: System Info + ja-JP: システム情報 + priority: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + ssl: ^_^ssl^_^ + url: /ISAPI/System/deviceInfo + method: GET + timeout: ^_^timeout^_^ + authorization: + type: Digest Auth + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: xmlPath + parseScript: //DeviceInfo + fields: + - field: deviceName + type: 1 + i18n: + zh-CN: 设备名称 + en-US: Device Name + ja-JP: デバイス名 + - field: deviceID + type: 1 + i18n: + zh-CN: 设备ID + en-US: Device ID + ja-JP: デバイスID + - field: firmwareVersion + type: 1 + i18n: + zh-CN: 固件版本 + en-US: Firmware Version + ja-JP: ファームウェアバージョン + - field: model + type: 1 + i18n: + zh-CN: 设备型号 + en-US: Device Model + ja-JP: デバイスモデル + - field: macAddress + type: 1 + i18n: + zh-CN: mac地址 + en-US: Mac Address + ja-JP: Macアドレス + - name: status + i18n: + zh-CN: 设备状态 + en-US: Status + ja-JP: 状態 + priority: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + ssl: ^_^ssl^_^ + url: /ISAPI/System/status + method: GET + timeout: ^_^timeout^_^ + authorization: + type: Digest Auth + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: xmlPath + parseScript: //DeviceStatus + fields: + - field: CPU_utilization + i18n: + zh-CN: CPU 利用率 + en-US: CPU Utilization + ja-JP: CPU使用率 + type: 0 + unit: '%' + - field: memory_usage + i18n: + zh-CN: 内存使用量 + en-US: Memory Usage + ja-JP: メモリ使用量 + type: 0 + unit: MB + - field: memory_available + i18n: + zh-CN: 可用内存 + en-US: Memory Available + ja-JP: 使用可能のメモリ + type: 0 + unit: MB + - field: cache_size + i18n: + zh-CN: 缓存大小 + en-US: Cache Size + ja-JP: キャッシュサイズ + type: 0 + unit: MB + - field: net_port_1_speed + i18n: + zh-CN: 网口1速度 + en-US: Net Port 1 Speed + ja-JP: ネットポート1速度 + type: 0 + unit: Mbps + - field: net_port_2_speed + i18n: + zh-CN: 网口2速度 + en-US: Net Port 2 Speed + ja-JP: ネットポート2速度 + type: 0 + unit: Mbps + - field: boot_time + i18n: + zh-CN: 启动时间 + en-US: Boot Time + ja-JP: 起動時間 + type: 1 + - field: device_uptime + i18n: + zh-CN: 运行时长 + en-US: Device Uptime + ja-JP: デバイスアップタイム + type: 1 + - field: last_calibration_time + i18n: + zh-CN: 上次校时时间 + en-US: Last Calibration Time + ja-JP: 最終校正時刻 + type: 1 + - field: last_calibration_time_diff + i18n: + zh-CN: 上次校时时间差 + en-US: Last Calibration Time Diff + ja-JP: 最終校正時間差 + type: 0 + unit: s + - field: avg_upload_time + i18n: + zh-CN: 平均上传耗时 + en-US: Avg Upload Time + ja-JP: 平均アップロード時間 + type: 0 + unit: ms + - field: max_upload_time + i18n: + zh-CN: 最大上传耗时 + en-US: Max Upload Time + ja-JP: 最大アップロード時間 + type: 0 + unit: ms + - field: min_upload_time + i18n: + zh-CN: 最小上传耗时 + en-US: Min Upload Time + ja-JP: 最小アップロード時間 + type: 0 + unit: ms + - field: last_calibration_mode + i18n: + zh-CN: 上次校时模式 + en-US: Last Calibration Mode + ja-JP: 最終校正モード + type: 1 + - field: last_calibration_address + i18n: + zh-CN: 上次校时地址 + en-US: Last Calibration Address + ja-JP: 最終校正アドレス + type: 1 + - field: response_time + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + type: 0 + unit: ms + aliasFields: + - CPUList/CPU/cpuUtilization + - MemoryList/Memory/memoryUsage + - MemoryList/Memory/memoryAvailable + - MemoryList/Memory/cacheSize + - NetPortStatusList/NetPortStatus[id='1']/workSpeed + - NetPortStatusList/NetPortStatus[id='2']/workSpeed + - bootTime + - deviceUpTime + - lastCalibrationTime + - lastCalibrationTimeDiff + - uploadTimeConsumingList/avgTime + - uploadTimeConsumingList/maxTime + - uploadTimeConsumingList/minTime + - lastCalibrationTimeMode + - lastCalibrationTimeAddress + - responseTime + calculates: + - CPU_utilization=CPUList/CPU/cpuUtilization + - memory_usage=MemoryList/Memory/memoryUsage + - memory_available=MemoryList/Memory/memoryAvailable + - cache_size=MemoryList/Memory/cacheSize + - net_port_1_speed=NetPortStatusList/NetPortStatus[id='1']/workSpeed + - net_port_2_speed=NetPortStatusList/NetPortStatus[id='2']/workSpeed + - boot_time=bootTime + - device_uptime=deviceUpTime + - last_calibration_time=lastCalibrationTime + - last_calibration_time_diff=lastCalibrationTimeDiff + - avg_upload_time=uploadTimeConsumingList/avgTime + - max_upload_time=uploadTimeConsumingList/maxTime + - min_upload_time=uploadTimeConsumingList/minTime + - last_calibration_mode=lastCalibrationTimeMode + - last_calibration_address=lastCalibrationTimeAddress + - response_time=responseTime + units: + - memory_usage=KB->MB + - memory_available=KB->MB + - cache_size=KB->MB \ No newline at end of file diff --git a/hertzbeat-manager/src/main/resources/define/app-hive.yml b/hertzbeat-manager/src/main/resources/define/app-hive.yml new file mode 100644 index 0000000..b13662c --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-hive.yml @@ -0,0 +1,368 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: springboot2 linux windows tomcat mysql aws... +app: hive +# The monitoring i18n name +name: + zh-CN: Apache Hive + en-US: Apache Hive + ja-JP: Apache Hive +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 <a class='help_module_content' href='https://cwiki.apache.org/confluence/display/Hive/Hive+Metrics'> HServer2 </a> 暴露的通用性能指标(basic、environment、thread、code_cache)进行采集监控。<span class='help_module_span'>⚠️注意:如果要监控 Apache Hive 中的信息,需要您的 Apache Hive 应用集成并开启 Hive Server2, <a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/hive'>点击查看具体步骤</a>。</span> + en-US: HertzBeat collects and monitors Apache Hive through general performance metric(health, environment, threads, memory_used) that exposed by the Hive Server2. <br><span class='help_module_span'>⚠️Note:You should make sure that your Apache Hive application have already integrated and enabled the Hive Server2, <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/hive'>click here to see the specific steps.</a></span> + zh-TW: HertzBeat 對<a class='help_module_content' href='https://cwiki.apache.org/confluence/display/Hive/Hive+Metrics'> HServer2 </a>暴露的通用性能指標(basic、environment、thread、code_cache)進行採集監控。<span class='help_module_span'> ⚠️ 注意:如果要監控Apache Hive中的指標,需要您的Apache Hive應用集成並開啟Hive Server2,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/hive'>點擊查看具體步驟</a>。</span> + ja-JP: HertzBeat は <a class='help_module_content' href='https://cwiki.apache.org/confluence/display/Hive/Hive+Metrics'> HServer2 </a> の一般的なパフォーマンスのメトリクスを監視します。<span class='help_module_span'>⚠️注意:Apache Hive で Hive Server2を有効にする必要があります。<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/hive'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/hive + en-US: https://hertzbeat.apache.org/docs/help/hive +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标 Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 10002 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL + # When the type is boolean, the frontend will display a switch for it. + type: boolean + # required-true or false + required: false + # field-param field key + - field: base_path + # name-param field display i18n name + name: + zh-CN: Base Path + en-US: Base Path + ja-JP: Base Path + # type-param field type(most mapping the html input type) The type "text" belongs to a text input field. + type: text + # default value + defaultValue: /jmx + # required-true or false + required: true + # hide-true or false + hide: true +# collect metrics config list +metrics: + # metrics - available + - name: available + i18n: + zh-CN: 可用性 + en-US: Availability + ja-JP: 可用性 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: responseTime + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + type: 0 + unit: ms + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^base_path^_^ + # http request method GET POST PUT DELETE PATCH + method: GET + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-api availability monitoring + parseType: default + + - name: basic + i18n: + zh-CN: 基本信息 + en-US: Basic + ja-JP: 基礎情報 + priority: 1 + fields: + - field: vm_name + i18n: + zh-CN: 虚拟机名称 + en-US: VM Name + ja-JP: 仮想マシン名 + type: 1 + - field: vm_vendor + i18n: + zh-CN: 虚拟机供应商 + en-US: VM Vendor + ja-JP: 仮想マシンベンダー + type: 1 + - field: vm_version + i18n: + zh-CN: 虚拟机版本 + en-US: VM Version + ja-JP: 仮想マシンバージョン + type: 1 + - field: up_time + i18n: + zh-CN: 运行时间 + en-US: Uptime + ja-JP: アップタイム + type: 0 + unit: ms + aliasFields: + - $.beans[?(@.name == 'java.lang:type=Runtime')].VmName + - $.beans[?(@.name == 'java.lang:type=Runtime')].VmVendor + - $.beans[?(@.name == 'java.lang:type=Runtime')].VmVersion + - $.beans[?(@.name == 'java.lang:type=Runtime')].Uptime + calculates: + - vm_name=$.beans[?(@.name == 'java.lang:type=Runtime')].VmName + - vm_vendor=$.beans[?(@.name == 'java.lang:type=Runtime')].VmVendor + - vm_version=$.beans[?(@.name == 'java.lang:type=Runtime')].VmVersion + - up_time=$.beans[?(@.name == 'java.lang:type=Runtime')].Uptime + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^ + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$' + + # metrics - environment + - name: environment + i18n: + zh-CN: 环境信息 + en-US: Environment + ja-JP: 環境 + priority: 2 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: https_proxyPort + i18n: + zh-CN: https 代理端口 + en-US: https Proxy Port + ja-JP: https プロキシポート + type: 0 + - field: os_name + i18n: + zh-CN: os 名称 + en-US: OS Name + ja-JP: OS名 + type: 1 + - field: os_version + i18n: + zh-CN: os 版本 + en-US: OS Version + ja-JP: OSバージョン + type: 1 + - field: os_arch + i18n: + zh-CN: os 架构 + en-US: OS Arch + ja-JP: OSアーキテクチャ + type: 1 + - field: java_runtime_name + i18n: + zh-CN: java 运行时名称 + en-US: Java Runtime Name + ja-JP: Java ランタイム名 + type: 1 + - field: java_runtime_version + i18n: + zh-CN: java 运行时版本 + en-US: Java Runtime Version + ja-JP: Java ランタイムバージョン + type: 1 + # metric alias list, used to identify metrics in query results + aliasFields: + - $.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'os.name')].value + - $.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'os.version')].value + - $.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'os.arch')].value + - $.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'java.runtime.name')].value + - $.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'java.runtime.version')].value + - $.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'https.proxyPort')].value + # A list of calculation scripts for metric values. + calculates: + - https_proxyPort=$.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'https.proxyPort')].value + - os_name=$.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'os.name')].value + - os_version=$.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'os.version')].value + - os_arch=$.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'os.arch')].value + - java_runtime_name=$.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'java.runtime.name')].value + - java_runtime_version=$.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'java.runtime.version')].value + # The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^base_path^_^ + # http request method GET POST PUT DELETE PATCH + method: GET + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-api availability monitoring + parseType: jsonPath + # http response data parse script + parseScript: '$' + + - name: thread + i18n: + zh-CN: 线程 + en-US: Thread + ja-JP: スレッド + priority: 3 + fields: + - field: thread_count + i18n: + zh-CN: 线程数 + en-US: Thread Count + ja-JP: スレッド総数 + type: 0 + - field: total_started_thread + i18n: + zh-CN: 启动线程数 + en-US: Total Started Thread + ja-JP: スレッド開始数 + type: 0 + - field: peak_thread_count + i18n: + zh-CN: 峰值线程数 + en-US: Peak Thread Count + ja-JP: ピークスレッド数 + type: 0 + - field: daemon_thread_count + i18n: + zh-CN: 守护线程数 + en-US: Daemon Thread Count + ja-JP: デーモンスレッド数 + type: 0 + aliasFields: + - $.beans[?(@.name == 'java.lang:type=Threading')].ThreadCount + - $.beans[?(@.name == 'java.lang:type=Threading')].TotalStartedThreadCount + - $.beans[?(@.name == 'java.lang:type=Threading')].PeakThreadCount + - $.beans[?(@.name == 'java.lang:type=Threading')].DaemonThreadCount + calculates: + - thread_count=$.beans[?(@.name == 'java.lang:type=Threading')].ThreadCount + - total_started_thread=$.beans[?(@.name == 'java.lang:type=Threading')].TotalStartedThreadCount + - peak_thread_count=$.beans[?(@.name == 'java.lang:type=Threading')].PeakThreadCount + - daemon_thread_count=$.beans[?(@.name == 'java.lang:type=Threading')].DaemonThreadCount + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^ + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$' + + - name: code_cache + i18n: + zh-CN: 代码缓存 + en-US: Code Cache + ja-JP: コードキャッシュ + priority: 4 + fields: + - field: committed + i18n: + zh-CN: 已提交 + en-US: Committed + ja-JP: コミット + type: 1 + unit: MB + - field: init + i18n: + zh-CN: 初始化 + en-US: Init + ja-JP: イニシャル + type: 0 + unit: MB + - field: max + i18n: + zh-CN: 最大 + en-US: Max + ja-JP: 最大 + type: 0 + unit: MB + - field: used + i18n: + zh-CN: 已使用 + en-US: Used + ja-JP: 使用済み + type: 0 + unit: MB + aliasFields: + - $.beans[?(@.Name == 'Code Cache')].Usage.committed + - $.beans[?(@.Name == 'Code Cache')].Usage.init + - $.beans[?(@.Name == 'Code Cache')].Usage.max + - $.beans[?(@.Name == 'Code Cache')].Usage.used + calculates: + - committed=$.beans[?(@.Name == 'Code Cache')].Usage.committed + - init=$.beans[?(@.Name == 'Code Cache')].Usage.init + - max=$.beans[?(@.Name == 'Code Cache')].Usage.max + - used=$.beans[?(@.Name == 'Code Cache')].Usage.used + units: + - committed=B->MB + - init=B->MB + - max=B->MB + - used=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^ + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$' diff --git a/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml b/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml new file mode 100644 index 0000000..efaf72a --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml @@ -0,0 +1,463 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: network +# The monitoring type eg: linux windows tomcat mysql aws... +app: hpe_switch +# The monitoring i18n name +name: + zh-CN: HPE通用交换机 + en-US: HPE Switch + ja-JP: HPEスイッチングハブ +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 协议</a> 对 HPE交换机 的通用指标(可用性,系统信息,端口流量等)进行采集监控。<br>您可以点击 “<i>新建 HPE通用交换机</i>” 并进行配置SNMP相关参数添加,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP Protocol</a> to monitoring HPE Switch general performance metrics. <br>You can click the "<i>New HPE Switch</i>" button and config snmp params to add monitor or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 協議</a> 對 HPE交換機 的通用指標(可用性,系統信息,端口流量等)進行采集監控。<br>您可以點擊 “<i>新建 HPE通用交換機</i>” 並進行配置SNMP相關參數添加,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP プロトコルを介して</a> HPEスイッチングハブの一般的なメトリクスを監視します。<br>「<i>新規 HPEスイッチングハブ</i>」をクリックしてSNMPなどのパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/hpe_switch + en-US: https://hertzbeat.apache.org/docs/help/hpe_switch +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 161 + # field-param field key + - field: snmpVersion + # name-param field display i18n name + name: + zh-CN: SNMP 版本 + en-US: SNMP Version + ja-JP: SNMPバージョン + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: SNMPv1 + value: 0 + - label: SNMPv2c + value: 1 + - label: SNMPv3 + value: 3 + # field-param field key + - field: community + # name-param field display i18n name + name: + zh-CN: SNMP 团体字 + en-US: SNMP Community + ja-JP: SNMPコミュニティ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp community for v1 v2c' + # dependent parameter values list + depend: + snmpVersion: + - 0 + - 1 + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: SNMP username + en-US: SNMP username + ja-JP: SNMPユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp username for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: contextName + # name-param field display i18n name + name: + zh-CN: SNMP contextName + en-US: SNMP contextName + ja-JP: SNMPコンテキスト名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp contextName for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP authPassword + en-US: SNMP authPassword + ja-JP: SNMP認証パスワード + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPasswordEncryption + # name-param field display i18n name + name: + zh-CN: authPassword 加密方式 + en-US: authPassword Encryption + ja-JP: 認証暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: MD5 + value: 0 + - label: SHA1 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP privPassphrase + en-US: SNMP privPassphrase + ja-JP: SNMPパスワードフレーズ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPasswordEncryption + # name-param field display i18n name + name: + zh-CN: privPassword 加密方式 + en-US: privPassword Encryption + ja-JP: パスワードの暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: DES + value: 0 + - label: AES128 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: false + # hide-is hide this field and put it in advanced layout + hide: true + # default value + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - system + - name: system + i18n: + zh-CN: 系统信息 + en-US: System Info + ja-JP: システム情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: name + type: 1 + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: descr + type: 1 + i18n: + zh-CN: 描述信息 + en-US: Description + ja-JP: 説明 + - field: uptime + type: 1 + i18n: + zh-CN: 运行时长 + en-US: Uptime + ja-JP: アップタイム + - field: location + type: 1 + i18n: + zh-CN: 位置 + en-US: Location + ja-JP: 位置 + - field: contact + type: 1 + i18n: + zh-CN: 联系人 + en-US: Contact + ja-JP: 連絡先 + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: snmp + # the config content when protocol is snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + # snmp 团体字 + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: get + # metrics oids: metric_name - oid_value + oids: + name: 1.3.6.1.2.1.1.5.0 + descr: 1.3.6.1.2.1.1.1.0 + uptime: 1.3.6.1.2.1.1.3.0 + location: 1.3.6.1.2.1.1.6.0 + contact: 1.3.6.1.2.1.1.4.0 + + - name: interfaces + i18n: + zh-CN: 接口详情 + en-US: Interfaces Detail + ja-JP: ネットワークカード詳細 + priority: 1 + fields: + - field: index + type: 1 + i18n: + zh-CN: 编号 + en-US: Index + ja-JP: 番号 + - field: descr + type: 1 + label: true + i18n: + zh-CN: 接口名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: mtu + type: 0 + unit: 'byte' + i18n: + zh-CN: MTU + en-US: MTU + ja-JP: MTU + - field: speed + type: 0 + unit: 'MB/s' + i18n: + zh-CN: 接口速率 + en-US: Interface Speed + ja-JP: ネットワークカード速度 + - field: in_octets + type: 0 + unit: 'MByte' + i18n: + zh-CN: 入流量 + en-US: In Octets + ja-JP: 受信されたバイト数 + - field: in_discards + type: 0 + i18n: + zh-CN: 入丢包数 + en-US: In Discards + ja-JP: 受信されたパケットのロス数 + - field: in_errors + type: 0 + i18n: + zh-CN: 入错包数 + en-US: In Errors + ja-JP: 受信異常パケット数 + - field: out_octets + type: 0 + unit: 'MByte' + i18n: + zh-CN: 出流量 + en-US: Out Octets + ja-JP: 転送されたバイト数 + - field: out_discards + type: 0 + i18n: + zh-CN: 出丢包数 + en-US: Out Discards + ja-JP: 転送されたパケットのロス数 + - field: out_errors + type: 0 + i18n: + zh-CN: 出错包数 + en-US: Out Errors + ja-JP: 転送異常パケット数 + - field: admin_status + type: 1 + i18n: + zh-CN: 配置状态 + en-US: Config Status + ja-JP: 設定ステータス + - field: oper_status + type: 1 + i18n: + zh-CN: 当前状态 + en-US: Current Status + ja-JP: 現在のステータス + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - ifIndex + - ifDescr + - ifMtu + - ifSpeed + - ifInOctets + - ifInDiscards + - ifInErrors + - ifOutOctets + - ifOutDiscards + - ifOutErrors + - ifAdminStatus + - ifOperStatus + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - index=ifIndex + - descr=ifDescr + - mtu=ifMtu + - speed=ifSpeed / 1000 / 1000 + - in_octets=ifInOctets / 1000 / 1000 + - in_discards=ifInDiscards + - in_errors=ifInErrors + - out_octets=ifOutOctets / 1000 / 1000 + - out_discards=ifOutDiscards + - out_errors=ifOutErrors + - admin_status=ifAdminStatus + - oper_status=ifOperStatus + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + authPassphrase: ^_^authPassphrase^_^ + privPassphrase: ^_^privPassphrase^_^ + privPasswordEncryption: ^_^privPasswordEncryption^_^ + authPasswordEncryption: ^_^authPasswordEncryption^_^ + contextName: ^_^contextName^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + ifIndex: 1.3.6.1.2.1.2.2.1.1 + ifDescr: 1.3.6.1.2.1.2.2.1.2 + ifMtu: 1.3.6.1.2.1.2.2.1.4 + ifSpeed: 1.3.6.1.2.1.2.2.1.5 + ifInOctets: 1.3.6.1.2.1.2.2.1.10 + ifInDiscards: 1.3.6.1.2.1.2.2.1.13 + ifInErrors: 1.3.6.1.2.1.2.2.1.14 + ifOutOctets: 1.3.6.1.2.1.2.2.1.16 + ifOutDiscards: 1.3.6.1.2.1.2.2.1.19 + ifOutErrors: 1.3.6.1.2.1.2.2.1.20 + ifAdminStatus: 1.3.6.1.2.1.2.2.1.7 + ifOperStatus: 1.3.6.1.2.1.2.2.1.8 diff --git a/hertzbeat-manager/src/main/resources/define/app-http_sd.yml b/hertzbeat-manager/src/main/resources/define/app-http_sd.yml new file mode 100644 index 0000000..dc0527d --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-http_sd.yml @@ -0,0 +1,155 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: __system__ +# Monitoring application type name (consistent with file name) eg: linux windows tomcat mysql aws... +app: http_sd +# The app api i18n name +name: + zh-CN: Http Service Discovery + en-US: Http Service Discovery + ja-JP: Httpサービスディスカバリー +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: __sd_url__ + # name-param field display i18n name + name: + zh-CN: 服务发现地址 + en-US: Service Discovery Url + ja-JP: サービスディスカバリー Url + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true + # field-param field key + - field: __sd_authType__ + # name-param field display i18n name + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + # type-param field type(most mapping the html input type) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: Bearer Token + value: Bearer Token + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + # field-param field key + - field: __sd_token__ + # name-param field display i18n name + name: + zh-CN: 认证Token + en-US: Access Token + ja-JP: アクセストークン + # type-param field type(most mapping the html input type) + type: text + # dependent parameter values list + depend: + __sd_authType__: + - Bearer Token + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: __sd_username__ + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # dependent parameter values list + depend: + __sd_authType__: + - Basic Auth + - Digest Auth + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: __sd_password__ + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input type) + type: password + # dependent parameter values list + depend: + __sd_authType__: + - Basic Auth + - Digest Auth + # required-true or false + required: false + # hide param-true or false + hide: true + +metrics: + - name: target + i18n: + zh-CN: 监控目标 + en-US: Monitor Target + ja-JP: 監視対象 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: host + type: 1 + i18n: + zh-CN: Host + en-US: Host + ja-JP: ホスト + - field: port + type: 1 + i18n: + zh-CN: Port + en-US: Port + ja-JP: ポート + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http_sd + # the config content when protocol is http_sd + http_sd: + # http url + url: ^_^__sd_url__^_^ + # http method: GET POST PUT DELETE PATCH + method: GET + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: ^_^__sd_authType__^_^ + bearerTokenToken: ^_^__sd_token__^_^ + basicAuthUsername: ^_^__sd_username__^_^ + basicAuthPassword: ^_^__sd_password__^_^ + digestAuthUsername: ^_^__sd_username__^_^ + digestAuthPassword: ^_^__sd_password__^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: default diff --git a/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml b/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml new file mode 100644 index 0000000..a210c4e --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml @@ -0,0 +1,462 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: network +# The monitoring type eg: linux windows tomcat mysql aws... +app: huawei_switch +# The monitoring i18n name +name: + zh-CN: 华为通用交换机 + en-US: Huawei Switch + ja-JP: Huaweiスイッチングハブ +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 协议</a> 对 华为交换机 的通用指标(可用性,系统信息,端口流量等)进行采集监控。<br>您可以点击 “<i>新建 华为通用交换机</i>” 并进行配置SNMP相关参数添加,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP Protocol</a> to monitoring Huawei Switch general performance metrics. <br>You can click the "<i>New Huawei Switch</i>" button and config snmp params to add monitor or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 協議</a> 對 华为交換機 的通用指標(可用性,系統信息,端口流量等)進行采集監控。<br>您可以點擊 “<i>新建 华为通用交換機</i>” 並進行配置SNMP相關參數添加,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP プロトコルを介して</a> Huaweiスイッチングハブの一般的なメトリクスを監視します。<br>「<i>新規 Huaweiスイッチングハブ</i>」をクリックしてSNMPなどのパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/huawei_switch + en-US: https://hertzbeat.apache.org/docs/help/huawei_switch +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 161 + # field-param field key + - field: snmpVersion + # name-param field display i18n name + name: + zh-CN: SNMP 版本 + en-US: SNMP Version + ja-JP: SNMPバージョン + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: SNMPv1 + value: 0 + - label: SNMPv2c + value: 1 + - label: SNMPv3 + value: 3 + # field-param field key + - field: community + # name-param field display i18n name + name: + zh-CN: SNMP 团体字 + en-US: SNMP Community + ja-JP: SNMPコミュニティ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp community for v1 v2c' + # dependent parameter values list + depend: + snmpVersion: + - 0 + - 1 + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: SNMP username + en-US: SNMP username + ja-JP: SNMPユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp username for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: contextName + # name-param field display i18n name + name: + zh-CN: SNMP contextName + en-US: SNMP contextName + ja-JP: SNMPコンテキスト名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp contextName for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP authPassword + en-US: SNMP authPassword + ja-JP: SNMP認証パスワード + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPasswordEncryption + # name-param field display i18n name + name: + zh-CN: authPassword 加密方式 + en-US: authPassword Encryption + ja-JP: 認証暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: MD5 + value: 0 + - label: SHA1 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP privPassphrase + en-US: SNMP privPassphrase + ja-JP: SNMPパスワードフレーズ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPasswordEncryption + # name-param field display i18n name + name: + zh-CN: privPassword 加密方式 + en-US: privPassword Encryption + ja-JP: パスワードの暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: DES + value: 0 + - label: AES128 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: false + # hide-is hide this field and put it in advanced layout + hide: true + # default value + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - system + - name: system + i18n: + zh-CN: 系统信息 + en-US: System Info + ja-JP: システム情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: name + type: 1 + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: descr + type: 1 + i18n: + zh-CN: 描述信息 + en-US: Description + ja-JP: 説明 + - field: uptime + type: 1 + i18n: + zh-CN: 运行时长 + en-US: Uptime + ja-JP: アップタイム + - field: location + type: 1 + i18n: + zh-CN: 位置 + en-US: Location + ja-JP: 位置 + - field: contact + type: 1 + i18n: + zh-CN: 联系人 + en-US: Contact + ja-JP: 連絡先 + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: snmp + # the config content when protocol is snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: get + # metrics oids: metric_name - oid_value + oids: + name: 1.3.6.1.2.1.1.5.0 + descr: 1.3.6.1.2.1.1.1.0 + uptime: 1.3.6.1.2.1.1.3.0 + location: 1.3.6.1.2.1.1.6.0 + contact: 1.3.6.1.2.1.1.4.0 + + - name: interfaces + i18n: + zh-CN: 接口详情 + en-US: Interfaces Detail + ja-JP: ネットワークカード詳細 + priority: 1 + fields: + - field: index + type: 1 + i18n: + zh-CN: 编号 + en-US: Index + ja-JP: 番号 + - field: descr + type: 1 + label: true + i18n: + zh-CN: 接口名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: mtu + type: 0 + unit: 'byte' + i18n: + zh-CN: MTU + en-US: MTU + ja-JP: MTU + - field: speed + type: 0 + unit: 'MB/s' + i18n: + zh-CN: 接口速率 + en-US: Interface Speed + ja-JP: ネットワークカード速度 + - field: in_octets + type: 0 + unit: 'MByte' + i18n: + zh-CN: 入流量 + en-US: In Octets + ja-JP: 受信されたバイト数 + - field: in_discards + type: 0 + i18n: + zh-CN: 入丢包数 + en-US: In Discards + ja-JP: 受信されたパケットのロス数 + - field: in_errors + type: 0 + i18n: + zh-CN: 入错包数 + en-US: In Errors + ja-JP: 受信異常パケット数 + - field: out_octets + type: 0 + unit: 'MByte' + i18n: + zh-CN: 出流量 + en-US: Out Octets + ja-JP: 転送されたバイト数 + - field: out_discards + type: 0 + i18n: + zh-CN: 出丢包数 + en-US: Out Discards + ja-JP: 転送されたパケットのロス数 + - field: out_errors + type: 0 + i18n: + zh-CN: 出错包数 + en-US: Out Errors + ja-JP: 転送異常パケット数 + - field: admin_status + type: 1 + i18n: + zh-CN: 配置状态 + en-US: Config Status + ja-JP: 設定ステータス + - field: oper_status + type: 1 + i18n: + zh-CN: 当前状态 + en-US: Current Status + ja-JP: 現在のステータス + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - ifIndex + - ifDescr + - ifMtu + - ifSpeed + - ifInOctets + - ifInDiscards + - ifInErrors + - ifOutOctets + - ifOutDiscards + - ifOutErrors + - ifAdminStatus + - ifOperStatus + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - index=ifIndex + - descr=ifDescr + - mtu=ifMtu + - speed=ifSpeed / 1000 / 1000 + - in_octets=ifInOctets / 1000 / 1000 + - in_discards=ifInDiscards + - in_errors=ifInErrors + - out_octets=ifOutOctets / 1000 / 1000 + - out_discards=ifOutDiscards + - out_errors=ifOutErrors + - admin_status=ifAdminStatus + - oper_status=ifOperStatus + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + authPassphrase: ^_^authPassphrase^_^ + privPassphrase: ^_^privPassphrase^_^ + privPasswordEncryption: ^_^privPasswordEncryption^_^ + authPasswordEncryption: ^_^authPasswordEncryption^_^ + contextName: ^_^contextName^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + ifIndex: 1.3.6.1.2.1.2.2.1.1 + ifDescr: 1.3.6.1.2.1.2.2.1.2 + ifMtu: 1.3.6.1.2.1.2.2.1.4 + ifSpeed: 1.3.6.1.2.1.2.2.1.5 + ifInOctets: 1.3.6.1.2.1.2.2.1.10 + ifInDiscards: 1.3.6.1.2.1.2.2.1.13 + ifInErrors: 1.3.6.1.2.1.2.2.1.14 + ifOutOctets: 1.3.6.1.2.1.2.2.1.16 + ifOutDiscards: 1.3.6.1.2.1.2.2.1.19 + ifOutErrors: 1.3.6.1.2.1.2.2.1.20 + ifAdminStatus: 1.3.6.1.2.1.2.2.1.7 + ifOperStatus: 1.3.6.1.2.1.2.2.1.8 diff --git a/hertzbeat-manager/src/main/resources/define/app-hugegraph.yml b/hertzbeat-manager/src/main/resources/define/app-hugegraph.yml new file mode 100644 index 0000000..747d5e0 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-hugegraph.yml @@ -0,0 +1,991 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: hugegraph linux windows tomcat mysql aws... +app: hugegraph +# The monitoring i18n name +name: + zh-CN: HugeGraph + en-US: HugeGraph + ja-JP: HugeGraph +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 HugeGraph 节点监控指标进行监控。<br>您可以点击 “<i>新建 Apache HugeGraph</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors the HugeGraph metrics. <br>You can click "<i>New Apache HugeGraph</i>" to configure, or select "<i>More Actions</i>" to import an existing configuration. + zh-TW: Hertzbeat 對 HugeGraph 節點監控指標進行監控。<br>您可以點擊 “<i>新建 Apache HugeGraph</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は HugeGraphノードの一般的なメトリクスを監視します。<br>「<i>新規 Apache HugeGraph</i>」をクリックしてパラメタを設定した後、新規することができます。 + +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/hugegraph + en-US: https://hertzbeat.apache.org/docs/help/hugegraph +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 8080 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL + # When the type is boolean, the frontend will display a switch for it. + type: boolean + # required-true or false + required: false + # field-param field key + - field: base_path + # name-param field display i18n name + name: + zh-CN: Base Path + en-US: Base Path + ja-JP: Base Path + # type-param field type(most mapping the html input type) The type "text" belongs to a text input field. + type: text + # default value + defaultValue: /metrics + # required-true or false + required: true + # hide-true or false + hide: true +# collect metrics config list +metrics: + # metrics - environment + - name: gauges + priority: 0 + # The specific monitoring metrics in the metric group. + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: edge-hugegraph-capacity + type: 0 + i18n: + zh-CN: edge-hugegraph-capacity + en-US: edge-hugegraph-capacity + ja-JP: edge-hugegraph容量 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: edge-hugegraph-expire + type: 0 + i18n: + zh-CN: edge-hugegraph-expire + en-US: edge-hugegraph-expire + ja-JP: edge-hugegraph期限切れ数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: edge-hugegraph-hits + type: 0 + i18n: + zh-CN: edge-hugegraph-hits + en-US: edge-hugegraph-hits + ja-JP: edge-hugegraphヒット数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: edge-hugegraph-miss + type: 0 + i18n: + zh-CN: edge-hugegraph-miss + en-US: edge-hugegraph-miss + ja-JP: edge-hugegraphミス数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: edge-hugegraph-size + type: 0 + i18n: + zh-CN: edge-hugegraph-size + en-US: edge-hugegraph-size + ja-JP: edge-hugegraphサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: instances + type: 0 + i18n: + zh-CN: instances + en-US: instances + ja-JP: instances + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: schema-id-hugegraph-capacity + type: 0 + i18n: + zh-CN: schema-id-hugegraph-capacity + en-US: schema-id-hugegraph-capacity + ja-JP: schema-id-hugegraph容量 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: schema-id-hugegraph-expire + type: 0 + i18n: + zh-CN: schema-id-hugegraph-expire + en-US: schema-id-hugegraph-expire + ja-JP: schema-id-hugegraph期限切れ数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: schema-id-hugegraph-hits + type: 0 + i18n: + zh-CN: schema-id-hugegraph-hits + en-US: schema-id-hugegraph-hits + ja-JP: schema-id-hugegraphヒット数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: schema-id-hugegraph-miss + type: 0 + i18n: + zh-CN: schema-id-hugegraph-miss + en-US: schema-id-hugegraph-miss + ja-JP: schema-id-hugegraphミス数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: schema-id-hugegraph-size + type: 0 + i18n: + zh-CN: schema-id-hugegraph-size + en-US: schema-id-hugegraph-size + ja-JP: schema-id-hugegraphサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: schema-name-hugegraph-capacity + type: 0 + i18n: + zh-CN: schema-name-hugegraph-capacity + en-US: schema-name-hugegraph-capacity + ja-JP: schema-name-hugegraph容量 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: schema-name-hugegraph-expire + type: 0 + i18n: + zh-CN: schema-name-hugegraph-expire + en-US: schema-name-hugegraph-expire + ja-JP: schema-name-hugegraph期限切れ数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: schema-name-hugegraph-hits + type: 0 + i18n: + zh-CN: schema-name-hugegraph-hits + en-US: schema-name-hugegraph-hits + ja-JP: schema-name-hugegraphヒット数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: schema-name-hugegraph-miss + type: 0 + i18n: + zh-CN: schema-name-hugegraph-miss + en-US: schema-name-hugegraph-miss + ja-JP: schema-name-hugegraphミス数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: schema-name-hugegraph-size + type: 0 + i18n: + zh-CN: schema-name-hugegraph-size + en-US: schema-name-hugegraph-size + ja-JP: schema-name-hugegraphサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: token-hugegraph-capacity + type: 0 + i18n: + zh-CN: token-hugegraph-capacity + en-US: token-hugegraph-capacity + ja-JP: token-hugegraph容量 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: token-hugegraph-expire + type: 0 + i18n: + zh-CN: token-hugegraph-expire + en-US: token-hugegraph-expire + ja-JP: token-hugegraph期限切れ数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: token-hugegraph-hits + type: 0 + i18n: + zh-CN: token-hugegraph-hits + en-US: token-hugegraph-hits + ja-JP: token-hugegraphヒット数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: token-hugegraph-miss + type: 0 + i18n: + zh-CN: token-hugegraph-miss + en-US: token-hugegraph-miss + ja-JP: token-hugegraphミス数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: token-hugegraph-size + type: 0 + i18n: + zh-CN: token-hugegraph-size + en-US: token-hugegraph-size + ja-JP: token-hugegraphサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: users-hugegraph-capacity + type: 0 + i18n: + zh-CN: users-hugegraph-capacity + en-US: users-hugegraph-capacity + ja-JP: users-hugegraph容量 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: users-hugegraph-expire + type: 0 + i18n: + zh-CN: users-hugegraph-expire + en-US: users-hugegraph-expire + ja-JP: users-hugegraph期限切れ数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: users-hugegraph-hits + type: 0 + i18n: + zh-CN: users-hugegraph-hits + en-US: users-hugegraph-hits + ja-JP: users-hugegraphヒット数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: users-hugegraph-miss + type: 0 + i18n: + zh-CN: users-hugegraph-miss + en-US: users-hugegraph-miss + ja-JP: users-hugegraphミス数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: users-hugegraph-size + type: 0 + i18n: + zh-CN: users-hugegraph-size + en-US: users-hugegraph-size + ja-JP: users-hugegraphサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: users_pwd-hugegraph-capacity + type: 0 + i18n: + zh-CN: users_pwd-hugegraph-capacity + en-US: users_pwd-hugegraph-capacity + ja-JP: users_pwd-hugegraph容量 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: users_pwd-hugegraph-expire + type: 0 + i18n: + zh-CN: users_pwd-hugegraph-expire + en-US: users_pwd-hugegraph-expire + ja-JP: users_pwd-hugegraph期限切れ数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: users_pwd-hugegraph-hits + type: 0 + i18n: + zh-CN: users_pwd-hugegraph-hits + en-US: users_pwd-hugegraph-hits + ja-JP: users_pwd-hugegraphヒット数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: users_pwd-hugegraph-miss + type: 0 + i18n: + zh-CN: users_pwd-hugegraph-miss + en-US: users_pwd-hugegraph-miss + ja-JP: users_pwd-hugegraphミス数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: users_pwd-hugegraph-size + type: 0 + i18n: + zh-CN: users_pwd-hugegraph-size + en-US: users_pwd-hugegraph-size + ja-JP: users_pwd-hugegraphサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: vertex-hugegraph-capacity + type: 0 + i18n: + zh-CN: vertex-hugegraph-capacity + en-US: vertex-hugegraph-capacity + ja-JP: vertex-hugegraph容量 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: vertex-hugegraph-expire + type: 0 + i18n: + zh-CN: vertex-hugegraph-expire + en-US: vertex-hugegraph-expire + ja-JP: vertex-hugegraph期限切れ数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: vertex-hugegraph-hits + type: 0 + i18n: + zh-CN: vertex-hugegraph-hits + en-US: vertex-hugegraph-hits + ja-JP: vertex-hugegraphヒット数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: vertex-hugegraph-miss + type: 0 + i18n: + zh-CN: vertex-hugegraph-miss + en-US: vertex-hugegraph-miss + ja-JP: vertex-hugegraphミス数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: vertex-hugegraph-size + type: 0 + i18n: + zh-CN: vertex-hugegraph-size + en-US: vertex-hugegraph-size + ja-JP: vertex-hugegraphサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: batch-write-threads + type: 0 + i18n: + zh-CN: batch-write-threads + en-US: batch-write-threads + ja-JP: 一括書き込みスレッド数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: max-write-threads + type: 0 + i18n: + zh-CN: max-write-threads + en-US: max-write-threads + ja-JP: 最大書き込みスレッド数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: pending-tasks + type: 0 + i18n: + zh-CN: pending-tasks + en-US: pending-tasks + ja-JP: ペンディングタスク数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: workers + type: 0 + i18n: + zh-CN: workers + en-US: workers + ja-JP: ワーカー数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: average-load-penalty + type: 0 + i18n: + zh-CN: average-load-penalty + en-US: average-load-penalty + ja-JP: 平均負荷ペナルティ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: estimated-size + type: 0 + i18n: + zh-CN: estimated-size + en-US: estimated-size + ja-JP: 推定サイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: eviction-count + type: 0 + i18n: + zh-CN: eviction-count + en-US: eviction-count + ja-JP: 立ち退き数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: eviction-weight + type: 0 + i18n: + zh-CN: eviction-weight + en-US: eviction-weight + ja-JP: 立ち退きウェイト + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hit-count + type: 0 + i18n: + zh-CN: hit-count + en-US: hit-count + ja-JP: ヒット数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hit-rate + type: 0 + i18n: + zh-CN: hit-rate + en-US: hit-rate + ja-JP: ヒット率 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: load-count + type: 0 + i18n: + zh-CN: load-count + en-US: load-count + ja-JP: ロード数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: load-failure-count + type: 0 + i18n: + zh-CN: load-failure-count + en-US: load-failure-count + ja-JP: ロード失敗数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: load-failure-rate + type: 0 + i18n: + zh-CN: load-failure-rate + en-US: load-failure-rate + ja-JP: ロード失敗率 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: load-success-count + type: 0 + i18n: + zh-CN: load-success-count + en-US: load-success-count + ja-JP: ロード成功数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: long-run-compilation-count + type: 0 + i18n: + zh-CN: long-run-compilation-count + en-US: long-run-compilation-count + ja-JP: long-run-compilation-count + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: miss-count + type: 0 + i18n: + zh-CN: miss-count + en-US: miss-count + ja-JP: ミス数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: miss-rate + type: 0 + i18n: + zh-CN: miss-rate + en-US: miss-rate + ja-JP: ミス率 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: request-count + type: 0 + i18n: + zh-CN: request-count + en-US: request-count + ja-JP: リクエスト数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: total-load-time + type: 0 + i18n: + zh-CN: total-load-time + en-US: total-load-time + ja-JP: ロード時間合計 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: sessions + type: 0 + i18n: + zh-CN: sessions + en-US: sessions + ja-JP: セッション + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.['org.apache.hugegraph.backend.cache.Cache.edge-hugegraph.capacity'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.edge-hugegraph.expire'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.edge-hugegraph.hits'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.edge-hugegraph.miss'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.edge-hugegraph.size'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.instances'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.schema-id-hugegraph.capacity'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.schema-id-hugegraph.expire'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.schema-id-hugegraph.hits'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.schema-id-hugegraph.miss'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.schema-id-hugegraph.size'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.schema-name-hugegraph.capacity'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.schema-name-hugegraph.expire'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.schema-name-hugegraph.hits'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.schema-name-hugegraph.miss'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.schema-name-hugegraph.size'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.token-hugegraph.capacity'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.token-hugegraph.expire'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.token-hugegraph.hits'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.token-hugegraph.miss'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.token-hugegraph.size'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.users-hugegraph.capacity'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.users-hugegraph.expire'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.users-hugegraph.hits'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.users-hugegraph.miss'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.users-hugegraph.size'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.users_pwd-hugegraph.capacity'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.users_pwd-hugegraph.expire'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.users_pwd-hugegraph.hits'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.users_pwd-hugegraph.miss'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.users_pwd-hugegraph.size'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.vertex-hugegraph.capacity'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.vertex-hugegraph.expire'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.vertex-hugegraph.hits'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.vertex-hugegraph.miss'].['value'] + - $.['org.apache.hugegraph.backend.cache.Cache.vertex-hugegraph.size'].['value'] + - $.['org.apache.hugegraph.server.RestServer.batch-write-threads'].['value'] + - $.['org.apache.hugegraph.server.RestServer.max-write-threads'].['value'] + - $.['org.apache.hugegraph.task.TaskManager.pending-tasks'].['value'] + - $.['org.apache.hugegraph.task.TaskManager.workers'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.average-load-penalty'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.estimated-size'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.eviction-count'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.eviction-weight'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.hit-count'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.hit-rate'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.load-count'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.load-failure-count'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.load-failure-rate'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.load-success-count'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.long-run-compilation-count'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.miss-count'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.miss-rate'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.request-count'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.total-load-time'].['value'] + - $.['org.apache.tinkerpop.gremlin.server.GremlinServer.sessions'].['value'] + calculates: + - edge-hugegraph-capacity=$.['org.apache.hugegraph.backend.cache.Cache.edge-hugegraph.capacity'].['value'] + - edge-hugegraph-expire=$.['org.apache.hugegraph.backend.cache.Cache.edge-hugegraph.expire'].['value'] + - edge-hugegraph-hits=$.['org.apache.hugegraph.backend.cache.Cache.edge-hugegraph.hits'].['value'] + - edge-hugegraph-miss=$.['org.apache.hugegraph.backend.cache.Cache.edge-hugegraph.miss'].['value'] + - edge-hugegraph-size=$.['org.apache.hugegraph.backend.cache.Cache.edge-hugegraph.size'].['value'] + - instances=$.['org.apache.hugegraph.backend.cache.Cache.instances'].['value'] + - schema-id-hugegraph-capacity=$.['org.apache.hugegraph.backend.cache.Cache.schema-id-hugegraph.capacity'].['value'] + - schema-id-hugegraph-expire=$.['org.apache.hugegraph.backend.cache.Cache.schema-id-hugegraph.expire'].['value'] + - schema-id-hugegraph-hits=$.['org.apache.hugegraph.backend.cache.Cache.schema-id-hugegraph.hits'].['value'] + - schema-id-hugegraph-miss=$.['org.apache.hugegraph.backend.cache.Cache.schema-id-hugegraph.miss'].['value'] + - schema-id-hugegraph-size=$.['org.apache.hugegraph.backend.cache.Cache.schema-id-hugegraph.size'].['value'] + - schema-name-hugegraph-capacity=$.['org.apache.hugegraph.backend.cache.Cache.schema-name-hugegraph.capacity'].['value'] + - schema-name-hugegraph-expire=$.['org.apache.hugegraph.backend.cache.Cache.schema-name-hugegraph.expire'].['value'] + - schema-name-hugegraph-hits=$.['org.apache.hugegraph.backend.cache.Cache.schema-name-hugegraph.hits'].['value'] + - schema-name-hugegraph-miss=$.['org.apache.hugegraph.backend.cache.Cache.schema-name-hugegraph.miss'].['value'] + - schema-name-hugegraph-size=$.['org.apache.hugegraph.backend.cache.Cache.schema-name-hugegraph.size'].['value'] + - token-hugegraph-capacity=$.['org.apache.hugegraph.backend.cache.Cache.token-hugegraph.capacity'].['value'] + - token-hugegraph-expire=$.['org.apache.hugegraph.backend.cache.Cache.token-hugegraph.expire'].['value'] + - token-hugegraph-hits=$.['org.apache.hugegraph.backend.cache.Cache.token-hugegraph.hits'].['value'] + - token-hugegraph-miss=$.['org.apache.hugegraph.backend.cache.Cache.token-hugegraph.miss'].['value'] + - token-hugegraph-size=$.['org.apache.hugegraph.backend.cache.Cache.token-hugegraph.size'].['value'] + - users-hugegraph-capacity=$.['org.apache.hugegraph.backend.cache.Cache.users-hugegraph.capacity'].['value'] + - users-hugegraph-expire=$.['org.apache.hugegraph.backend.cache.Cache.users-hugegraph.expire'].['value'] + - users-hugegraph-hits=$.['org.apache.hugegraph.backend.cache.Cache.users-hugegraph.hits'].['value'] + - users-hugegraph-miss=$.['org.apache.hugegraph.backend.cache.Cache.users-hugegraph.miss'].['value'] + - users-hugegraph-size=$.['org.apache.hugegraph.backend.cache.Cache.users-hugegraph.size'].['value'] + - users_pwd-hugegraph-capacity=$.['org.apache.hugegraph.backend.cache.Cache.users_pwd-hugegraph.capacity'].['value'] + - users_pwd-hugegraph-expire=$.['org.apache.hugegraph.backend.cache.Cache.users_pwd-hugegraph.expire'].['value'] + - users_pwd-hugegraph-hits=$.['org.apache.hugegraph.backend.cache.Cache.users_pwd-hugegraph.hits'].['value'] + - users_pwd-hugegraph-miss=$.['org.apache.hugegraph.backend.cache.Cache.users_pwd-hugegraph.miss'].['value'] + - users_pwd-hugegraph-size=$.['org.apache.hugegraph.backend.cache.Cache.users_pwd-hugegraph.size'].['value'] + - vertex-hugegraph-capacity=$.['org.apache.hugegraph.backend.cache.Cache.vertex-hugegraph.capacity'].['value'] + - vertex-hugegraph-expire=$.['org.apache.hugegraph.backend.cache.Cache.vertex-hugegraph.expire'].['value'] + - vertex-hugegraph-hits=$.['org.apache.hugegraph.backend.cache.Cache.vertex-hugegraph.hits'].['value'] + - vertex-hugegraph-miss=$.['org.apache.hugegraph.backend.cache.Cache.vertex-hugegraph.miss'].['value'] + - vertex-hugegraph-size=$.['org.apache.hugegraph.backend.cache.Cache.vertex-hugegraph.size'].['value'] + - batch-write-threads=$.['org.apache.hugegraph.server.RestServer.batch-write-threads'].['value'] + - max-write-threads=$.['org.apache.hugegraph.server.RestServer.max-write-threads'].['value'] + - pending-tasks=$.['org.apache.hugegraph.task.TaskManager.pending-tasks'].['value'] + - workers=$.['org.apache.hugegraph.task.TaskManager.workers'].['value'] + - average-load-penalty=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.average-load-penalty'].['value'] + - estimated-size=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.estimated-size'].['value'] + - eviction-count=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.eviction-count'].['value'] + - eviction-weight=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.eviction-weight'].['value'] + - hit-count=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.hit-count'].['value'] + - hit-rate=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.hit-rate'].['value'] + - load-count=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.load-count'].['value'] + - load-failure-count=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.load-failure-count'].['value'] + - load-failure-rate=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.load-failure-rate'].['value'] + - load-success-count=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.load-success-count'].['value'] + - long-run-compilation-count=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.long-run-compilation-count'].['value'] + - miss-count=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.miss-count'].['value'] + - miss-rate=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.miss-rate'].['value'] + - request-count=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.request-count'].['value'] + - total-load-time=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.gremlin-groovy.sessionless.class-cache.total-load-time'].['value'] + - sessions=$.['org.apache.tinkerpop.gremlin.server.GremlinServer.sessions'].['value'] + # The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^base_path^_^/gauges?type=json + # http request method GET POST PUT DELETE PATCH + method: GET + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-api availability monitoring + parseType: jsonPath + # http response data parse script + parseScript: '$' + + - name: counters + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: GET-SUCCESS_COUNTER + type: 0 + i18n: + zh-CN: GET-SUCCESS_COUNTER + en-US: GET-SUCCESS_COUNTER + ja-JP: GET成功カウンタ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: GET-TOTAL_COUNTER + type: 0 + i18n: + zh-CN: GET-TOTAL_COUNTER + en-US: GET-TOTAL_COUNTER + ja-JP: GETカウンタ合計 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: favicon-ico-GET-FAILED_COUNTER + type: 0 + i18n: + zh-CN: favicon-ico-GET-FAILED_COUNTER + en-US: favicon-ico-GET-FAILED_COUNTER + ja-JP: favicon-ico-GET失敗カウンタ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: favicon-ico-GET-TOTAL_COUNTER + type: 0 + i18n: + zh-CN: favicon-ico-GET-TOTAL_COUNTER + en-US: favicon-ico-GET-TOTAL_COUNTER + ja-JP: favicon-ico-GETカウンタ合計 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: graphs-HEAD-FAILED_COUNTER + type: 0 + i18n: + zh-CN: graphs-HEAD-FAILED_COUNTER + en-US: graphs-HEAD-FAILED_COUNTER + ja-JP: graphs-HEAD失敗カウンタ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: graphs-HEAD-SUCCESS_COUNTER + type: 0 + i18n: + zh-CN: graphs-HEAD-SUCCESS_COUNTER + en-US: graphs-HEAD-SUCCESS_COUNTER + ja-JP: graphs-HEAD成功カウンタ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: graphs-HEAD-TOTAL_COUNTER + type: 0 + i18n: + zh-CN: graphs-HEAD-TOTAL_COUNTER + en-US: graphs-HEAD-TOTAL_COUNTER + ja-JP: graphs-HEADカウンタ合計 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: graphs-hugegraph-graph-vertices-GET-SUCCESS_COUNTER + type: 0 + i18n: + zh-CN: graphs-hugegraph-graph-vertices-GET-SUCCESS_COUNTER + en-US: graphs-hugegraph-graph-vertices-GET-SUCCESS_COUNTER + ja-JP: graphs-hugegraph-graph-vertices-GET成功カウンタ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: graphs-hugegraph-graph-vertices-GET-TOTAL_COUNTER + type: 0 + i18n: + zh-CN: graphs-hugegraph-graph-vertices-GET-TOTAL_COUNTER + en-US: graphs-hugegraph-graph-vertices-GET-TOTAL_COUNTER + ja-JP: graphs-hugegraph-graph-vertices-GETカウンタ合計 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: metircs-GET-FAILED_COUNTER + type: 0 + i18n: + zh-CN: metircs-GET-FAILED_COUNTER + en-US: metircs-GET-FAILED_COUNTER + ja-JP: metircs-GET失敗カウンタ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: metircs-GET-TOTAL_COUNTER + type: 0 + i18n: + zh-CN: metircs-GET-TOTAL_COUNTER + en-US: metircs-GET-TOTAL_COUNTER + ja-JP: metircs-GETカウンタ合計 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: metrics-GET-SUCCESS_COUNTER + type: 0 + i18n: + zh-CN: metrics-GET-SUCCESS_COUNTER + en-US: metrics-GET-SUCCESS_COUNTER + ja-JP: metircs-GET成功カウンタ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: metrics-GET-TOTAL_COUNTER + type: 0 + i18n: + zh-CN: metrics-GET-TOTAL_COUNTER + en-US: metrics-GET-TOTAL_COUNTER + ja-JP: metircs-GETカウンタ合計 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: metrics-gauges-GET-SUCCESS_COUNTER + type: 0 + i18n: + zh-CN: metrics-gauges-GET-SUCCESS_COUNTER + en-US: metrics-gauges-GET-SUCCESS_COUNTER + ja-JP: metrics-gauges-GET成功カウンタ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: metrics-gauges-GET-TOTAL_COUNTER + type: 0 + i18n: + zh-CN: metrics-gauges-GET-TOTAL_COUNTER + en-US: metrics-gauges-GET-TOTAL_COUNTER + ja-JP: metrics-gauges-GETカウンタ合計 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.['//GET/SUCCESS_COUNTER'].['count'] + - $.['//GET/TOTAL_COUNTER'].['count'] + - $.['/favicon.ico/GET/FAILED_COUNTER'].['count'] + - $.['/favicon.ico/GET/TOTAL_COUNTER'].['count'] + - $.['/graphs/HEAD/FAILED_COUNTER'].['count'] + - $.['/graphs/HEAD/SUCCESS_COUNTER'].['count'] + - $.['/graphs/HEAD/TOTAL_COUNTER'].['count'] + - $.['/graphs/hugegraph/graph/vertices/GET/SUCCESS_COUNTER'].['count'] + - $.['/graphs/hugegraph/graph/vertices/GET/TOTAL_COUNTER'].['count'] + - $.['/metircs/GET/FAILED_COUNTER'].['count'] + - $.['/metircs/GET/TOTAL_COUNTER'].['count'] + - $.['/metrics/GET/SUCCESS_COUNTER'].['count'] + - $.['/metrics/GET/TOTAL_COUNTER'].['count'] + - $.['/metrics/gauges/GET/SUCCESS_COUNTER'].['count'] + - $.['/metrics/gauges/GET/TOTAL_COUNTER'].['count'] + calculates: + - GET-SUCCESS_COUNTER=$.['//GET/SUCCESS_COUNTER'].['count'] + - GET-TOTAL_COUNTER=$.['//GET/TOTAL_COUNTER'].['count'] + - favicon-ico-GET-FAILED_COUNTER=$.['/favicon.ico/GET/FAILED_COUNTER'].['count'] + - favicon-ico-GET-TOTAL_COUNTER=$.['/favicon.ico/GET/TOTAL_COUNTER'].['count'] + - graphs-HEAD-FAILED_COUNTER=$.['/graphs/HEAD/FAILED_COUNTER'].['count'] + - graphs-HEAD-SUCCESS_COUNTER=$.['/graphs/HEAD/SUCCESS_COUNTER'].['count'] + - graphs-HEAD-TOTAL_COUNTER=$.['/graphs/HEAD/TOTAL_COUNTER'].['count'] + - graphs-hugegraph-graph-vertices-GET-SUCCESS_COUNTER=$.['/graphs/hugegraph/graph/vertices/GET/SUCCESS_COUNTER'].['count'] + - graphs-hugegraph-graph-vertices-GET-TOTAL_COUNTER=$.['/graphs/hugegraph/graph/vertices/GET/TOTAL_COUNTER'].['count'] + - metircs-GET-FAILED_COUNTER=$.['/metircs/GET/FAILED_COUNTER'].['count'] + - metircs-GET-TOTAL_COUNTER=$.['/metircs/GET/TOTAL_COUNTER'].['count'] + - metrics-GET-SUCCESS_COUNTER=$.['/metrics/GET/SUCCESS_COUNTER'].['count'] + - metrics-GET-TOTAL_COUNTER=$.['/metrics/GET/TOTAL_COUNTER'].['count'] + - metrics-gauges-GET-SUCCESS_COUNTER=$.['/metrics/gauges/GET/SUCCESS_COUNTER'].['count'] + - metrics-gauges-GET-TOTAL_COUNTER=$.['/metrics/gauges/GET/TOTAL_COUNTER'].['count'] + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/counters?type=json + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$' + + - name: system + priority: 2 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: mem + type: 0 + i18n: + zh-CN: mem + en-US: mem + ja-JP: メモリ + - field: mem_total + type: 0 + i18n: + zh-CN: mem_total + en-US: mem_total + ja-JP: メモリ容量 + - field: mem_used + type: 0 + i18n: + zh-CN: mem_used + en-US: mem_used + ja-JP: 使用したメモリ + - field: mem_free + type: 0 + i18n: + zh-CN: mem_free + en-US: mem_free + ja-JP: 利用可能なメモリ + - field: mem_unit + type: 1 + i18n: + zh-CN: mem_unit + en-US: mem_unit + ja-JP: メモリ単位 + - field: processors + type: 0 + i18n: + zh-CN: 处理器数 + en-US: processors + ja-JP: プロセッサ数 + - field: uptime + type: 0 + i18n: + zh-CN: uptime + en-US: uptime + ja-JP: アップタイム + - field: systemload_average + type: 0 + i18n: + zh-CN: systemload_average + en-US: systemload_average + ja-JP: システムロードアベレージ + - field: heap_committed + type: 0 + i18n: + zh-CN: heap_committed + en-US: heap_committed + ja-JP: ヒープのコミットされたメモリ + - field: heap_init + type: 0 + i18n: + zh-CN: heap_init + en-US: heap_init + ja-JP: ヒープのイニシャルメモリ + - field: heap_used + type: 0 + i18n: + zh-CN: heap_used + en-US: heap_used + ja-JP: ヒープの使用したメモリ + - field: heap_max + type: 0 + i18n: + zh-CN: heap_max + en-US: heap_max + ja-JP: ヒープの最大メモリ + - field: nonheap_committed + type: 0 + i18n: + zh-CN: nonheap_committed + en-US: nonheap_committed + ja-JP: ノンヒープのコミットされたメモリ + - field: nonheap_init + type: 0 + i18n: + zh-CN: nonheap_init + en-US: nonheap_init + ja-JP: ノンヒープのイニシャルメモリ + - field: nonheap_used + type: 0 + i18n: + zh-CN: nonheap_used + en-US: nonheap_used + ja-JP: ノンヒープの使用したメモリ + - field: nonheap_max + type: 0 + i18n: + zh-CN: nonheap_max + en-US: nonheap_max + ja-JP: ノンヒープの最大メモリ + - field: thread_peak + type: 0 + i18n: + zh-CN: thread_peak + en-US: thread_peak + ja-JP: ピークスレッド数 + - field: thread_daemon + type: 0 + i18n: + zh-CN: thread_daemon + en-US: thread_daemon + ja-JP: デーモンスレッド数 + - field: thread_total_started + type: 0 + i18n: + zh-CN: thread_total_started + en-US: thread_total_started + ja-JP: スレッド開始数 + - field: thread_count + type: 0 + i18n: + zh-CN: thread_count + en-US: thread_count + ja-JP: スレッド総数 + - field: garbage_collector_g1_young_generation_count + type: 0 + i18n: + zh-CN: garbage_collector_g1_young_generation_count + en-US: garbage_collector_g1_young_generation_count + ja-JP: garbage_collector_g1若い領域GC回数 + - field: garbage_collector_g1_young_generation_time + type: 0 + i18n: + zh-CN: garbage_collector_g1_young_generation_time + en-US: garbage_collector_g1_young_generation_time + ja-JP: garbage_collector_g1若い領域GC時間 + - field: garbage_collector_g1_old_generation_count + type: 0 + i18n: + zh-CN: garbage_collector_g1_old_generation_count + en-US: garbage_collector_g1_old_generation_count + ja-JP: garbage_collector_g1古い領域GC回数 + - field: garbage_collector_g1_old_generation_time + type: 0 + i18n: + zh-CN: garbage_collector_g1_old_generation_time + en-US: garbage_collector_g1_old_generation_time + ja-JP: garbage_collector_g1古い領域GC時間 + - field: garbage_collector_time_unit + type: 1 + i18n: + zh-CN: garbage_collector_time_unit + en-US: garbage_collector_time_unit + ja-JP: garbage_collector時間単位 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.basic.mem + - $.basic.mem_total + - $.basic.mem_used + - $.basic.mem_free + - $.basic.mem_unit + - $.basic.processors + - $.basic.uptime + - $.basic.systemload_average + - $.heap.committed + - $.heap.init + - $.heap.used + - $.heap.max + - $.nonheap.committed + - $.nonheap.init + - $.nonheap.used + - $.nonheap.max + - $.thread.peak + - $.thread.daemon + - $.thread.total_started + - $.thread.count + - $.garbage_collector.g1_young_generation_count + - $.garbage_collector.g1_young_generation_time + - $.garbage_collector.g1_old_generation_count + - $.garbage_collector.g1_old_generation_time + - $.garbage_collector.time_unit + calculates: + - mem=$.basic.mem + - mem_total=$.basic.mem_total + - mem_used=$.basic.mem_used + - mem_free=$.basic.mem_free + - mem_unit=$.basic.mem_unit + - processors=$.basic.processors + - uptime=$.basic.uptime + - systemload_average=$.basic.systemload_average + - heap_committed=$.heap.committed + - heap_init=$.heap.init + - heap_used=$.heap.used + - heap_max=$.heap.max + - nonheap_committed=$.nonheap.committed + - nonheap_init=$.nonheap.init + - nonheap_used=$.nonheap.used + - nonheap_max=$.nonheap.max + - thread_peak=$.thread.peak + - thread_daemon=$.thread.daemon + - thread_total_started=$.thread.total_started + - thread_count=$.thread.count + - garbage_collector_g1_young_generation_count=$.garbage_collector.g1_young_generation_count + - garbage_collector_g1_young_generation_time=$.garbage_collector.g1_young_generation_time + - garbage_collector_g1_old_generation_count=$.garbage_collector.g1_old_generation_count + - garbage_collector_g1_old_generation_time=$.garbage_collector.g1_old_generation_time + - garbage_collector_time_unit=$.garbage_collector.time_unit + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/system?type=json + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$' diff --git a/hertzbeat-manager/src/main/resources/define/app-iceberg.yml b/hertzbeat-manager/src/main/resources/define/app-iceberg.yml new file mode 100644 index 0000000..de61ac5 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-iceberg.yml @@ -0,0 +1,368 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: springboot2 linux windows tomcat mysql aws... +app: iceberg +# The monitoring i18n name +name: + zh-CN: Apache Iceberg + en-US: Apache Iceberg + ja-JP: Apache Iceberg +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 <a class='help_module_content' href='https://cwiki.apache.org/confluence/display/Hive/Hive+Metrics'> HServer2 </a> 暴露的通用性能指标(basic、environment、thread、code_cache)进行采集监控。<span class='help_module_span'>⚠️注意:如果要监控 Apache Iceberg 中的信息,需要您的 Apache Iceberg 应用集成并开启 Hive Server2, <a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/hive'>点击查看具体步骤</a>。</span> + en-US: HertzBeat collects and monitors Apache Iceberg through general performance metric(health, environment, threads, memory_used) that exposed by the Hive Server2. <br><span class='help_module_span'>⚠️Note:You should make sure that your Apache Iceberg application have already integrated and enabled the Hive Server2, <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/hive'>click here to see the specific steps.</a></span> + zh-TW: HertzBeat 對<a class='help_module_content' href='https://cwiki.apache.org/confluence/display/Hive/Hive+Metrics'> HServer2 </a>暴露的通用性能指標(basic、environment、thread、code_cache)進行採集監控。<span class='help_module_span'> ⚠️ 注意:如果要監控Apache Iceberg 中的指標,需要您的Apache Iceberg 應用集成並開啟 Hive Server2,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/hive'>點擊查看具體步驟</a>。</span> + ja-JP: HertzBeat は <a class='help_module_content' href='https://cwiki.apache.org/confluence/display/Hive/Hive+Metrics'> HServer2 </a> の一般的なパフォーマンスのメトリクスを監視します。<span class='help_module_span'>⚠️注意:Apache Iceberg で Hive Server2を有効にする必要があります。<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/hive'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/iceberg + en-US: https://hertzbeat.apache.org/docs/help/iceberg +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 10002 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL + # When the type is boolean, the frontend will display a switch for it. + type: boolean + # required-true or false + required: false + # field-param field key + - field: base_path + # name-param field display i18n name + name: + zh-CN: Base Path + en-US: Base Path + ja-JP: Base Path + # type-param field type(most mapping the html input type) The type "text" belongs to a text input field. + type: text + # default value + defaultValue: /jmx + # required-true or false + required: true + # hide-true or false + hide: true +# collect metrics config list +metrics: + # metrics - available + - name: available + i18n: + zh-CN: 可用性 + en-US: Availability + ja-JP: 可用性 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: responseTime + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + type: 0 + unit: ms + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^base_path^_^ + # http request method GET POST PUT DELETE PATCH + method: GET + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-api availability monitoring + parseType: default + + - name: basic + i18n: + zh-CN: 基本信息 + en-US: Basic + ja-JP: 基礎情報 + priority: 1 + fields: + - field: vm_name + i18n: + zh-CN: 虚拟机名称 + en-US: VM Name + ja-JP: 仮想マシン名 + type: 1 + - field: vm_vendor + i18n: + zh-CN: 虚拟机供应商 + en-US: VM Vendor + ja-JP: 仮想マシンベンダー + type: 1 + - field: vm_version + i18n: + zh-CN: 虚拟机版本 + en-US: VM Version + ja-JP: 仮想マシンバージョン + type: 1 + - field: up_time + i18n: + zh-CN: 运行时间 + en-US: Uptime + ja-JP: アップタイム + type: 0 + unit: ms + aliasFields: + - $.beans[?(@.name == 'java.lang:type=Runtime')].VmName + - $.beans[?(@.name == 'java.lang:type=Runtime')].VmVendor + - $.beans[?(@.name == 'java.lang:type=Runtime')].VmVersion + - $.beans[?(@.name == 'java.lang:type=Runtime')].Uptime + calculates: + - vm_name=$.beans[?(@.name == 'java.lang:type=Runtime')].VmName + - vm_vendor=$.beans[?(@.name == 'java.lang:type=Runtime')].VmVendor + - vm_version=$.beans[?(@.name == 'java.lang:type=Runtime')].VmVersion + - up_time=$.beans[?(@.name == 'java.lang:type=Runtime')].Uptime + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^ + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$' + + # metrics - environment + - name: environment + i18n: + zh-CN: 环境信息 + en-US: Environment + ja-JP: 環境 + priority: 2 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: https_proxyPort + i18n: + zh-CN: https 代理端口 + en-US: https Proxy Port + ja-JP: https プロキシポート + type: 0 + - field: os_name + i18n: + zh-CN: os 名称 + en-US: OS Name + ja-JP: OS名 + type: 1 + - field: os_version + i18n: + zh-CN: os 版本 + en-US: OS Version + ja-JP: OSバージョン + type: 1 + - field: os_arch + i18n: + zh-CN: os 架构 + en-US: OS Arch + ja-JP: OSアーキテクチャ + type: 1 + - field: java_runtime_name + i18n: + zh-CN: java 运行时名称 + en-US: Java Runtime Name + ja-JP: Java ランタイム名 + type: 1 + - field: java_runtime_version + i18n: + zh-CN: java 运行时版本 + en-US: Java Runtime Version + ja-JP: Java ランタイムバージョン + type: 1 + # metric alias list, used to identify metrics in query results + aliasFields: + - $.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'os.name')].value + - $.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'os.version')].value + - $.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'os.arch')].value + - $.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'java.runtime.name')].value + - $.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'java.runtime.version')].value + - $.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'https.proxyPort')].value + # A list of calculation scripts for metric values. + calculates: + - https_proxyPort=$.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'https.proxyPort')].value + - os_name=$.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'os.name')].value + - os_version=$.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'os.version')].value + - os_arch=$.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'os.arch')].value + - java_runtime_name=$.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'java.runtime.name')].value + - java_runtime_version=$.beans[?(@.name == 'java.lang:type=Runtime')].SystemProperties[?(@.key == 'java.runtime.version')].value + # The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^base_path^_^ + # http request method GET POST PUT DELETE PATCH + method: GET + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-api availability monitoring + parseType: jsonPath + # http response data parse script + parseScript: '$' + + - name: thread + i18n: + zh-CN: 线程 + en-US: Thread + ja-JP: スレッド + priority: 3 + fields: + - field: thread_count + i18n: + zh-CN: 线程数 + en-US: Thread Count + ja-JP: スレッド総数 + type: 0 + - field: total_started_thread + i18n: + zh-CN: 启动线程数 + en-US: Total Started Thread + ja-JP: スレッド開始数 + type: 0 + - field: peak_thread_count + i18n: + zh-CN: 峰值线程数 + en-US: Peak Thread Count + ja-JP: ピークスレッド数 + type: 0 + - field: daemon_thread_count + i18n: + zh-CN: 守护线程数 + en-US: Daemon Thread Count + ja-JP: デーモンスレッド数 + type: 0 + aliasFields: + - $.beans[?(@.name == 'java.lang:type=Threading')].ThreadCount + - $.beans[?(@.name == 'java.lang:type=Threading')].TotalStartedThreadCount + - $.beans[?(@.name == 'java.lang:type=Threading')].PeakThreadCount + - $.beans[?(@.name == 'java.lang:type=Threading')].DaemonThreadCount + calculates: + - thread_count=$.beans[?(@.name == 'java.lang:type=Threading')].ThreadCount + - total_started_thread=$.beans[?(@.name == 'java.lang:type=Threading')].TotalStartedThreadCount + - peak_thread_count=$.beans[?(@.name == 'java.lang:type=Threading')].PeakThreadCount + - daemon_thread_count=$.beans[?(@.name == 'java.lang:type=Threading')].DaemonThreadCount + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^ + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$' + + - name: code_cache + i18n: + zh-CN: 代码缓存 + en-US: Code Cache + ja-JP: コードキャッシュ + priority: 4 + fields: + - field: committed + i18n: + zh-CN: 已提交 + en-US: Committed + ja-JP: コミット + type: 1 + unit: MB + - field: init + i18n: + zh-CN: 初始化 + en-US: Init + ja-JP: イニシャル + type: 0 + unit: MB + - field: max + i18n: + zh-CN: 最大 + en-US: Max + ja-JP: 最大 + type: 0 + unit: MB + - field: used + i18n: + zh-CN: 已使用 + en-US: Used + ja-JP: 使用済み + type: 0 + unit: MB + aliasFields: + - $.beans[?(@.Name == 'Code Cache')].Usage.committed + - $.beans[?(@.Name == 'Code Cache')].Usage.init + - $.beans[?(@.Name == 'Code Cache')].Usage.max + - $.beans[?(@.Name == 'Code Cache')].Usage.used + calculates: + - committed=$.beans[?(@.Name == 'Code Cache')].Usage.committed + - init=$.beans[?(@.Name == 'Code Cache')].Usage.init + - max=$.beans[?(@.Name == 'Code Cache')].Usage.max + - used=$.beans[?(@.Name == 'Code Cache')].Usage.used + units: + - committed=B->MB + - init=B->MB + - max=B->MB + - used=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^ + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$' diff --git a/hertzbeat-manager/src/main/resources/define/app-idrac.yml b/hertzbeat-manager/src/main/resources/define/app-idrac.yml new file mode 100644 index 0000000..24f9c55 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-idrac.yml @@ -0,0 +1,797 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: server +# The monitoring type eg: linux windows tomcat mysql aws... +app: idrac +# The monitoring i18n name +name: + zh-CN: Dell iDRAC + en-US: Dell iDRAC + ja-JP: Dell iDRAC +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 协议</a> 对 Dell iDRAC 的通用指标(可用性,系统信息,传感器状态等)进行采集监控。<br>您可以点击 “<i>新建 Dell iDRAC</i>” 并进行配置SNMP相关参数添加,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP Protocol</a> to monitoring Dell iDRAC general performance metrics. <br>You can click the "<i>New Dell iDRAC</i>" button and config snmp params to add monitor or import an existing setup through the "<i>More Actions</i>" menu. + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP プロトコルを介して</a> Dell iDRACの一般的なメトリクスを監視します。<br>「<i>新規 Dell iDRAC</i>」をクリックしてSNMPなどのパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/idrac + en-US: https://hertzbeat.apache.org/docs/help/idrac +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 161 + # field-param field key + - field: snmpVersion + # name-param field display i18n name + name: + zh-CN: SNMP 版本 + en-US: SNMP Version + ja-JP: SNMPバージョン + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: SNMPv1 + value: 0 + - label: SNMPv2c + value: 1 + - label: SNMPv3 + value: 3 + # field-param field key + - field: community + # name-param field display i18n name + name: + zh-CN: SNMP 团体字 + en-US: SNMP Community + ja-JP: SNMPコミュニティ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp community for v1 v2c' + # dependent parameter values list + depend: + snmpVersion: + - 0 + - 1 + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: SNMP username + en-US: SNMP username + ja-JP: SNMPユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp username for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: contextName + # name-param field display i18n name + name: + zh-CN: SNMP contextName + en-US: SNMP contextName + ja-JP: SNMPコンテキスト名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp contextName for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP authPassword + en-US: SNMP authPassword + ja-JP: SNMP認証パスワード + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPasswordEncryption + # name-param field display i18n name + name: + zh-CN: authPassword 加密方式 + en-US: authPassword Encryption + ja-JP: 認証暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: MD5 + value: 0 + - label: SHA1 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP privPassphrase + en-US: SNMP privPassphrase + ja-JP: SNMPパスワードフレーズ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPasswordEncryption + # name-param field display i18n name + name: + zh-CN: privPassword 加密方式 + en-US: privPassword Encryption + ja-JP: パスワードの暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: DES + value: 0 + - label: AES128 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: false + # hide-is hide this field and put it in advanced layout + hide: true + # default value + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - system + - name: system + i18n: + zh-CN: 系统状态 + en-US: System Status + ja-JP: システム情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: global + type: 1 + i18n: + zh-CN: 全局系统状态 + en-US: Global System Status + ja-JP: システム状態 + - field: lcd + type: 1 + i18n: + zh-CN: 系统前面板状态 + en-US: System LCD Status + ja-JP: システムLCDの状態 + - field: storage + type: 1 + i18n: + zh-CN: 全局存储状态 + en-US: Global Storage Status + ja-JP: ストレージ状態 + - field: power + type: 1 + i18n: + zh-CN: 系统电源状态 + en-US: System Power State + ja-JP: システム電源状態 + - field: up_time + type: 1 + unit: day + i18n: + zh-CN: 系统运行时间 + en-US: System Power Up Time + ja-JP: アップタイム + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - globalSystemStatusEnum + - systemLCDStatusEnum + - globalStorageStatusEnum + - systemPowerStateEnum + - systemPowerUpTime + + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - "global=(globalSystemStatusEnum == 1) ? 'other' : (globalSystemStatusEnum == 2) ? 'unknown' : (globalSystemStatusEnum == 3) ? 'ok' : (globalSystemStatusEnum == 4) ? 'nonCritical' : (globalSystemStatusEnum == 5) ? 'critical' : (globalSystemStatusEnum == 6) ? 'nonRecoverable' : 'unknown'" + - "lcd=(systemLCDStatusEnum == 1) ? 'other' : (systemLCDStatusEnum == 2) ? 'unknown' : (systemLCDStatusEnum == 3) ? 'ok' : (systemLCDStatusEnum == 4) ? 'nonCritical' : (systemLCDStatusEnum == 5) ? 'critical' : (systemLCDStatusEnum == 6) ? 'nonRecoverable' : 'unknown'" + - "storage=(globalStorageStatusEnum == 1) ? 'other' : (globalStorageStatusEnum == 2) ? 'unknown' : (globalStorageStatusEnum == 3) ? 'ok' : (globalStorageStatusEnum == 4) ? 'nonCritical' : (globalStorageStatusEnum == 5) ? 'critical' : (globalStorageStatusEnum == 6) ? 'nonRecoverable' : 'unknown'" + - "power=(systemPowerStateEnum == 1) ? 'other' : (systemPowerStateEnum == 2) ? 'unknown' : (systemPowerStateEnum == 3) ? 'off' : (systemPowerStateEnum == 4) ? 'ok' : 'unknown'" + - up_time=systemPowerUpTime/60/60/24 + + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: snmp + # the config content when protocol is snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: get + # metrics oids: metric_name - oid_value + oids: + globalSystemStatusEnum: 1.3.6.1.4.1.674.10892.5.2.1.0 + systemLCDStatusEnum: 1.3.6.1.4.1.674.10892.5.2.2.0 + globalStorageStatusEnum: 1.3.6.1.4.1.674.10892.5.2.3.0 + systemPowerStateEnum: 1.3.6.1.4.1.674.10892.5.2.4.0 + systemPowerUpTime: 1.3.6.1.4.1.674.10892.5.2.5.0 + + - name: power_supply + i18n: + zh-CN: 电源供应器 + en-US: Power Supply + ja-JP: 電源供給ユニット + priority: 1 + fields: + - field: index + type: 1 + i18n: + zh-CN: 索引 + en-US: Index + ja-JP: 番号 + - field: name + type: 1 + label: true + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: wattage + type: 1 + unit: W + i18n: + zh-CN: 额定功率 + en-US: Wattage + ja-JP: ワット数 + - field: input_wattage + type: 0 + unit: W + i18n: + zh-CN: 输入功率 + en-US: Input Wattage + ja-JP: 入力ワット数 + - field: max_input_voltage + type: 1 + unit: V + i18n: + zh-CN: 最大输入电压 + en-US: Max Input Voltage + ja-JP: 最大入力電圧 + - field: type + type: 1 + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: タイプ + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: 状態 + aliasFields: + - powerSupplyIndex + - powerSupplyLocationName + - powerSupplyStatusEnum + - powerSupplyOutputWatts + - powerSupplyTypeEnum + - powerSupplyRatedInputWattage + - powerSupplyMaximumInputVoltage + calculates: + - "index=powerSupplyIndex" + - "name=powerSupplyLocationName" + - "status=(powerSupplyStatusEnum == 1) ? 'other' : (powerSupplyStatusEnum == 2) ? 'unknown' : (powerSupplyStatusEnum == 3) ? 'ok' : (powerSupplyStatusEnum == 4) ? 'nonCritical' : (powerSupplyStatusEnum == 5) ? 'critical' : (powerSupplyStatusEnum == 6) ? 'nonRecoverable' : 'unknown'" + - "location=powerSupplyLocationName" + - "wattage=powerSupplyOutputWatts / 10" + - "type=(powerSupplyTypeEnum == 1) ? 'other' : (powerSupplyTypeEnum == 2) ? 'unknown' : (powerSupplyTypeEnum == 3) ? 'Linear' : (powerSupplyTypeEnum == 4) ? 'Switching' : (powerSupplyTypeEnum == 5) ? 'Battery' : (powerSupplyTypeEnum == 6) ? 'UPS' : (powerSupplyTypeEnum == 7) ? 'Converter' : (powerSupplyTypeEnum == 8) ? 'Regulator' : (powerSupplyTypeEnum == 9) ? 'AC' : (powerSupplyTypeEnum == 10) ? 'DC' : (powerSupplyTypeEnum == 11) ? 'VRM' : 'unknown'" + - "input_wattage=powerSupplyRatedInputWattage / 10" + - "max_input_voltage=powerSupplyMaximumInputVoltage" + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + authPassphrase: ^_^authPassphrase^_^ + privPassphrase: ^_^privPassphrase^_^ + privPasswordEncryption: ^_^privPasswordEncryption^_^ + authPasswordEncryption: ^_^authPasswordEncryption^_^ + contextName: ^_^contextName^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + powerSupplyIndex: 1.3.6.1.4.1.674.10892.5.4.600.12.1.2 + powerSupplyLocationName: 1.3.6.1.4.1.674.10892.5.4.600.12.1.8 + powerSupplyStatusEnum: 1.3.6.1.4.1.674.10892.5.4.600.12.1.5 + powerSupplyTypeEnum: 1.3.6.1.4.1.674.10892.5.4.600.12.1.7 + powerSupplyOutputWatts: 1.3.6.1.4.1.674.10892.5.4.600.12.1.6 + powerSupplyRatedInputWattage: 1.3.6.1.4.1.674.10892.5.4.600.12.1.14 + powerSupplyMaximumInputVoltage: 1.3.6.1.4.1.674.10892.5.4.600.12.1.9 + + - name: cooling + i18n: + zh-CN: 冷却设备 + en-US: Cooling Devices + ja-JP: クーリング情報 + priority: 1 + fields: + - field: index + type: 1 + i18n: + zh-CN: 索引 + en-US: Index + ja-JP: 番号 + - field: name + type: 1 + label: true + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: type + type: 1 + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: タイプ + - field: reading + type: 0 + unit: RPM + i18n: + zh-CN: 转速 + en-US: Reading + ja-JP: 回転数 + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: 状態 + aliasFields: + - coolingDeviceIndex + - coolingDeviceLocationName + - coolingDeviceStatusEnum + - coolingDeviceReading + - coolingDeviceTypeEnum + calculates: + - "index=coolingDeviceIndex" + - "name=coolingDeviceLocationName" + - "status=(coolingDeviceStatusEnum == 1) ? 'other' : (coolingDeviceStatusEnum == 2) ? 'unknown' : (coolingDeviceStatusEnum == 3) ? 'ok' : (coolingDeviceStatusEnum == 4) ? 'nonCriticalUpper' : (coolingDeviceStatusEnum == 5) ? 'criticalUpper' : (coolingDeviceStatusEnum == 6) ? 'nonRecoverableUpper' : (coolingDeviceStatusEnum == 7) ? 'noncriticalLower' : (coolingDeviceStatusEnum == 8) ? 'criticalLower' : (coolingDeviceStatusEnum == 9) ? 'nonRecoverableLower' : (coolingDeviceStatusEnum == 10) ? 'failed' : 'unknown'" + - "reading=coolingDeviceReading" + - "type=(coolingDeviceTypeEnum == 1) ? 'other' : (coolingDeviceTypeEnum == 2) ? 'unknown' : (coolingDeviceTypeEnum == 3) ? 'AFan' : (coolingDeviceTypeEnum == 4) ? 'ABlower' : (coolingDeviceTypeEnum == 5) ? 'AChipFan' : (coolingDeviceTypeEnum == 6) ? 'ACabinetFan' : (coolingDeviceTypeEnum == 7) ? 'APowerSupplyFan' : (coolingDeviceTypeEnum == 8) ? 'AHeatPipe' : (coolingDeviceTypeEnum == 9) ? 'Refrigeration' : (coolingDeviceTypeEnum == 10) ? 'ActiveCooling' : (coolingDeviceTypeEnum == 11) ? 'PassiveCooling' : 'unknown'" + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + authPassphrase: ^_^authPassphrase^_^ + privPassphrase: ^_^privPassphrase^_^ + privPasswordEncryption: ^_^privPasswordEncryption^_^ + authPasswordEncryption: ^_^authPasswordEncryption^_^ + contextName: ^_^contextName^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + coolingDeviceIndex: 1.3.6.1.4.1.674.10892.5.4.700.12.1.2 + coolingDeviceLocationName: 1.3.6.1.4.1.674.10892.5.4.700.12.1.8 + coolingDeviceStatusEnum: 1.3.6.1.4.1.674.10892.5.4.700.12.1.5 + coolingDeviceReading: 1.3.6.1.4.1.674.10892.5.4.700.12.1.6 + coolingDeviceTypeEnum: 1.3.6.1.4.1.674.10892.5.4.700.12.1.7 + + - name: temperature + i18n: + zh-CN: 温度传感器 + en-US: Temperature Sensors + ja-JP: 内部温度情報 + priority: 1 + fields: + - field: index + type: 1 + i18n: + zh-CN: 索引 + en-US: Index + ja-JP: 番号 + - field: name + type: 1 + label: true + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: reading + type: 0 + unit: °C + i18n: + zh-CN: 温度 + en-US: Temperature + ja-JP: 温度 + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: 状態 + aliasFields: + - temperatureProbeIndex + - temperatureProbeLocationName + - temperatureProbeStatusEnum + - temperatureProbeReading + calculates: + - "index=temperatureProbeIndex" + - "name=temperatureProbeLocationName" + - "status=(temperatureProbeStatusEnum == 1) ? 'other' : (temperatureProbeStatusEnum == 2) ? 'unknown' : (temperatureProbeStatusEnum == 3) ? 'ok' : (temperatureProbeStatusEnum == 4) ? 'nonCriticalUpper' : (temperatureProbeStatusEnum == 5) ? 'criticalUpper' : (temperatureProbeStatusEnum == 6) ? 'nonRecoverableUpper' : (temperatureProbeStatusEnum == 7) ? 'noncriticalLower' : (temperatureProbeStatusEnum == 8) ? 'criticalLower' : (temperatureProbeStatusEnum == 9) ? 'nonRecoverableLower' : (temperatureProbeStatusEnum == 10) ? 'failed' : 'unknown'" + - "reading=temperatureProbeReading / 10" + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + authPassphrase: ^_^authPassphrase^_^ + privPassphrase: ^_^privPassphrase^_^ + privPasswordEncryption: ^_^privPasswordEncryption^_^ + authPasswordEncryption: ^_^authPasswordEncryption^_^ + contextName: ^_^contextName^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + temperatureProbeIndex: 1.3.6.1.4.1.674.10892.5.4.700.20.1.2 + temperatureProbeLocationName: 1.3.6.1.4.1.674.10892.5.4.700.20.1.8 + temperatureProbeStatusEnum: 1.3.6.1.4.1.674.10892.5.4.700.20.1.5 + temperatureProbeReading: 1.3.6.1.4.1.674.10892.5.4.700.20.1.6 + + - name: voltage + i18n: + zh-CN: 电压传感器 + en-US: Voltage Sensors + ja-JP: 電圧情報 + priority: 1 + fields: + - field: index + type: 1 + i18n: + zh-CN: 索引 + en-US: Index + ja-JP: 番号 + - field: name + type: 1 + label: true + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: reading + type: 0 + unit: V + i18n: + zh-CN: 电压 + en-US: Voltage + ja-JP: 電圧 + - field: type + type: 1 + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: タイプ + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: 状態 + aliasFields: + - voltageProbeIndex + - voltageProbeLocationName + - voltageProbeStatusEnum + - voltageProbeReading + - voltageProbeTypeEnum + calculates: + - "index=voltageProbeIndex" + - "name=voltageProbeLocationName" + - "status=(voltageProbeStatusEnum == 1) ? 'other' : (voltageProbeStatusEnum == 2) ? 'unknown' : (voltageProbeStatusEnum == 3) ? 'ok' : (voltageProbeStatusEnum == 4) ? 'nonCriticalUpper' : (voltageProbeStatusEnum == 5) ? 'criticalUpper' : (voltageProbeStatusEnum == 6) ? 'nonRecoverableUpper' : (voltageProbeStatusEnum == 7) ? 'nonCriticalLower' : (voltageProbeStatusEnum == 8) ? 'criticalLower' : (voltageProbeStatusEnum == 9) ? 'nonRecoverableLower' : (voltageProbeStatusEnum == 10) ? 'failed' : 'unknown'" + - "reading=voltageProbeReading / 1000" + - "type=(voltageProbeTypeEnum == 1) ? 'other' : (voltageProbeTypeEnum == 2) ? 'unknown' : (voltageProbeTypeEnum == 3) ? '1.5V' : (voltageProbeTypeEnum == 4) ? '3.3V' : (voltageProbeTypeEnum == 5) ? '5V' : (voltageProbeTypeEnum == 6) ? '-5V' : (voltageProbeTypeEnum == 7) ? '12V' : (voltageProbeTypeEnum == 8) ? '-12V' : (voltageProbeTypeEnum == 9) ? 'IO' : (voltageProbeTypeEnum == 10) ? 'Core' : (voltageProbeTypeEnum == 11) ? 'FLEA' : (voltageProbeTypeEnum == 12) ? 'Battery' : (voltageProbeTypeEnum == 13) ? 'Terminator' : (voltageProbeTypeEnum == 14) ? '2.5V' : (voltageProbeTypeEnum == 15) ? 'GTL' : (voltageProbeTypeEnum == 16) ? 'Discrete' : (voltageProbeTypeEnum == 17) ? 'GenericDiscrete' : (voltageProbeTypeEnum == 18) ? 'PSVoltage' : (voltageProbeTypeEnum == 19) ? 'MemoryStatus' : 'unknown'" + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + authPassphrase: ^_^authPassphrase^_^ + privPassphrase: ^_^privPassphrase^_^ + privPasswordEncryption: ^_^privPasswordEncryption^_^ + authPasswordEncryption: ^_^authPasswordEncryption^_^ + contextName: ^_^contextName^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + voltageProbeIndex: 1.3.6.1.4.1.674.10892.5.4.600.20.1.2 + voltageProbeLocationName: 1.3.6.1.4.1.674.10892.5.4.600.20.1.8 + voltageProbeStatusEnum: 1.3.6.1.4.1.674.10892.5.4.600.20.1.5 + voltageProbeReading: 1.3.6.1.4.1.674.10892.5.4.600.20.1.6 + voltageProbeTypeEnum: 1.3.6.1.4.1.674.10892.5.4.600.20.1.7 + + + - name: memory + i18n: + zh-CN: 内存设备 + en-US: Memory Devices + ja-JP: メモリ情報 + priority: 1 + fields: + - field: index + type: 1 + i18n: + zh-CN: 索引 + en-US: Index + ja-JP: 番号 + - field: name + type: 1 + label: true + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: type + type: 1 + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: タイプ + - field: sizes + type: 1 + unit: G + i18n: + zh-CN: 容量 + en-US: Size + ja-JP: メモリ容量 + - field: speed + type: 1 + unit: MHz + i18n: + zh-CN: 频率 + en-US: Speed + ja-JP: メモリの最大速度 + - field: current_speed + type: 0 + unit: MHz + i18n: + zh-CN: 运行频率 + en-US: Current Speed + ja-JP: 現在のメモリ速度 + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: 状態 + aliasFields: + - memoryDeviceIndex + - memoryDeviceLocationName + - memoryDeviceStatusEnum + - memoryDeviceSize + - memoryDeviceTypeEnum + - memoryDeviceSpeed + - memoryDeviceCurrentOperatingSpeed + calculates: + - "index=memoryDeviceIndex" + - "name=memoryDeviceLocationName" + - "status=(memoryDeviceStatusEnum == 1) ? 'other' : (memoryDeviceStatusEnum == 2) ? 'unknown' : (memoryDeviceStatusEnum == 3) ? 'ok' : (memoryDeviceStatusEnum == 4) ? 'nonCritical' : (memoryDeviceStatusEnum == 5) ? 'critical' : (memoryDeviceStatusEnum == 6) ? 'nonRecoverable' : 'unknown'" + - "sizes=memoryDeviceSize/1024/1024" + - "type=(memoryDeviceTypeEnum == 1) ? 'other' : (memoryDeviceTypeEnum == 2) ? 'unknown' : (memoryDeviceTypeEnum == 3) ? 'DRAM' : (memoryDeviceTypeEnum == 4) ? 'EDRAM' : (memoryDeviceTypeEnum == 5) ? 'VRAM' : (memoryDeviceTypeEnum == 6) ? 'SRAM' : (memoryDeviceTypeEnum == 7) ? 'RAM' : (memoryDeviceTypeEnum == 8) ? 'ROM' : (memoryDeviceTypeEnum == 9) ? 'FLASH' : (memoryDeviceTypeEnum == 10) ? 'EEPROM' : (memoryDeviceTypeEnum == 11) ? 'FEPROM' : (memoryDeviceTypeEnum == 12) ? 'EPROM' : (memoryDeviceTypeEnum == 13) ? 'CDRAM' : (memoryDeviceTypeEnum == 14) ? '3DRAM' : (memoryDeviceTypeEnum == 15) ? 'SDRAM' : (memoryDeviceTypeEnum == 16) ? 'SGRAM' : (memoryDeviceTypeEnum == 17) ? 'RDRAM' : (memoryDeviceTypeEnum == 18) ? 'DDR' : (memoryDeviceTypeEnum == 19) ? 'DDR2' : (memoryDeviceTypeEnum == 20) ? 'DDR2FBDIMM' : (memoryDeviceTypeEnum == 24) ? 'DDR3' : (memoryDeviceTypeEnum == 25) ? 'FBD2' : (memoryDeviceTypeEnum == 26) ? 'DDR4' : 'unknown'" + - "speed=memoryDeviceSpeed" + - "current_speed=memoryDeviceCurrentOperatingSpeed" + + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + authPassphrase: ^_^authPassphrase^_^ + privPassphrase: ^_^privPassphrase^_^ + privPasswordEncryption: ^_^privPasswordEncryption^_^ + authPasswordEncryption: ^_^authPasswordEncryption^_^ + contextName: ^_^contextName^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + memoryDeviceIndex: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.2 + memoryDeviceLocationName: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.8 + memoryDeviceStatusEnum: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.5 + memoryDeviceSize: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.14 + memoryDeviceTypeEnum: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.7 + memoryDeviceSpeed: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.15 + memoryDeviceCurrentOperatingSpeed: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.27 + + - name: processor + i18n: + zh-CN: 处理器 + en-US: Processors + ja-JP: プロセッサ情報 + priority: 1 + fields: + - field: index + type: 1 + i18n: + zh-CN: 索引 + en-US: Index + ja-JP: 番号 + - field: name + type: 1 + label: true + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: speed + type: 0 + unit: MHz + i18n: + zh-CN: 当前频率 + en-US: Current Speed + ja-JP: プロセッサ速度 + - field: family + type: 1 + i18n: + zh-CN: 家族 + en-US: Family + ja-JP: ファミリー + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: 状態 + aliasFields: + - processorDeviceIndex + - processorDeviceLocationName + - processorDeviceStatusEnum + - processorDeviceSpeed + - processorDeviceFamilyEnum + calculates: + - "index=processorDeviceIndex" + - "name=processorDeviceLocationName" + - "status=(processorDeviceStatusEnum == 1) ? 'other' : (processorDeviceStatusEnum == 2) ? 'unknown' : (processorDeviceStatusEnum == 3) ? 'ok' : (processorDeviceStatusEnum == 4) ? 'nonCritical' : (processorDeviceStatusEnum == 5) ? 'critical' : (processorDeviceStatusEnum == 6) ? 'nonRecoverable' : 'unknown'" + - "speed=processorDeviceSpeed" + - "family=(processorDeviceFamilyEnum == 1) ? 'other' : (processorDeviceFamilyEnum == 2) ? 'unknown' : (processorDeviceFamilyEnum == 3) ? '8086' : (processorDeviceFamilyEnum == 4) ? '80286' : (processorDeviceFamilyEnum == 5) ? 'Intel386' : (processorDeviceFamilyEnum == 6) ? 'Intel486' : (processorDeviceFamilyEnum == 11) ? 'Pentium' : (processorDeviceFamilyEnum == 12) ? 'PentiumPro' : (processorDeviceFamilyEnum == 13) ? 'PentiumII' : (processorDeviceFamilyEnum == 14) ? 'PentiumMMX' : (processorDeviceFamilyEnum == 15) ? 'Celeron' : (processorDeviceFamilyEnum == 16) ? 'PentiumIIXeon' : (processorDeviceFamilyEnum == 17) ? 'PentiumIII' : (processorDeviceFamilyEnum == 18) ? 'PentiumIIIXeon' : (processorDeviceFamilyEnum == 19) ? 'PentiumIIISpeedStep' : (processorDeviceFamilyEnum == 20) ? 'Itanium' : (processorDeviceFamilyEnum == 21) ? 'IntelXeon' : (processorDeviceFamilyEnum == 22) ? 'Pentium4' : (processorDeviceFamilyEnum == 23) ? 'IntelXeonMP' : (processorDeviceFamilyEnum == 24) ? 'IntelItanium2' : (processorDeviceFamilyEnum == 40) ? 'IntelCoreDuo' : (processorDeviceFamilyEnum == 41) ? 'IntelCoreDuoMobile' : (processorDeviceFamilyEnum == 42) ? 'IntelCoreSoloMobile' : (processorDeviceFamilyEnum == 43) ? 'IntelAtom' : (processorDeviceFamilyEnum == 131) ? 'AMDAthlon64' : (processorDeviceFamilyEnum == 132) ? 'AMDOpteron' : (processorDeviceFamilyEnum == 134) ? 'AMDTurion64Mobile' : (processorDeviceFamilyEnum == 190) ? 'IntelCore2' : (processorDeviceFamilyEnum == 191) ? 'IntelCore2Duo' : (processorDeviceFamilyEnum == 194) ? 'IntelCore2Quad' : (processorDeviceFamilyEnum == 198) ? 'IntelCorei7' : (processorDeviceFamilyEnum == 205) ? 'IntelCorei5' : (processorDeviceFamilyEnum == 206) ? 'IntelCorei3' : 'unknown'" + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + authPassphrase: ^_^authPassphrase^_^ + privPassphrase: ^_^privPassphrase^_^ + privPasswordEncryption: ^_^privPasswordEncryption^_^ + authPasswordEncryption: ^_^authPasswordEncryption^_^ + contextName: ^_^contextName^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + processorDeviceIndex: 1.3.6.1.4.1.674.10892.5.4.1100.30.1.2 + processorDeviceLocationName: 1.3.6.1.4.1.674.10892.5.4.1100.30.1.8 + processorDeviceStatusEnum: 1.3.6.1.4.1.674.10892.5.4.1100.30.1.5 + processorDeviceSpeed: 1.3.6.1.4.1.674.10892.5.4.1100.30.1.12 + processorDeviceFamilyEnum: 1.3.6.1.4.1.674.10892.5.4.1100.30.1.10 diff --git a/hertzbeat-manager/src/main/resources/define/app-influxdb.yml b/hertzbeat-manager/src/main/resources/define/app-influxdb.yml new file mode 100644 index 0000000..ef554f2 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-influxdb.yml @@ -0,0 +1,381 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: influxdb +# The monitoring i18n name +name: + zh-CN: InfluxDB + en-US: InfluxDB + ja-JP: InfluxDB +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 InfluxDB 时序数据库进行监控。<br><span class='help_module_span'><a class='help_module_content' href='https://docs.influxdata.com/platform/monitoring/influxdata-platform/tools/measurements-internal'>点击查看开启步骤</a>。</span> + en-US: HertzBeat monitors the InfluxDB time series database. <br><span class='help_module_span'><a class='help_module_content' href='https://docs.influxdata.com/platform/monitoring/influxdata-platform/tools/measurements-internal '>Click to view the activation steps</a>. </span> + zh-TW: HertzBeat 對 InfluxDB 時序資料庫進行監控。<br><span class='help_module_span'><a class='help_module_content' href='https://docs.influxdata.com/platform/monitoring/influxdata-platform/tools/measurements-internal'>點擊查看開啓步驟</a>。</span> + ja-JP: HertzBeat は InfluxDB 時系列データベースを監視します。<br><span class='help_module_span'><a class='help_module_content' href='https://docs.influxdata.com/platform/monitoring/influxdata-platform/tools/measurements-internal'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/influxdb/ + en-US: https://hertzbeat.apache.org/docs/help/influxdb/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # default value + defaultValue: 8086 + required: true + - field: timeout + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + type: number + required: false + # hide param-true or false + hide: true + defaultValue: 6000 + +# collect metrics config list +metrics: + # metrics - cluster_node_status + + - name: influxdb_info + i18n: + zh-CN: influxdb 基本信息 + en-US: influxdb_info + ja-JP: influxdb基礎情報 + priority: 0 + fields: + - field: build_date + i18n: + zh-CN: 创建日期 + en-US: build_date + ja-JP: 作成日 + type: 1 + label: true + - field: os + i18n: + zh-CN: 操作系统 + en-US: os + ja-JP: オーエス + type: 1 + - field: cpus + i18n: + zh-CN: cpus + en-US: cpus + ja-JP: 使用可能のCPUコア数 + type: 1 + - field: version + i18n: + zh-CN: 版本 + en-US: version + ja-JP: バージョン + type: 1 + aliasFields: + - build_date + - os + - cpus + - version + calculates: + - build_date=build_date + - os=os + - cpus=cpus + - version=version + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: http_api_request_duration_seconds + i18n: + zh-CN: http 响应时间 + en-US: http_api_request_duration_seconds + ja-JP: http 応答時間 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: handler + i18n: + zh-CN: handler + en-US: handler + ja-JP: ハンドラ + type: 1 + - field: path + i18n: + zh-CN: 路径 + en-US: path + ja-JP: パス + type: 1 + - field: response_code + i18n: + zh-CN: 返回 code + en-US: response_code + ja-JP: 応答コード + type: 1 + - field: method + i18n: + zh-CN: 方法 + en-US: method + ja-JP: リクエストメソッド + type: 1 + - field: user_agent + i18n: + zh-CN: 用户代理 + en-US: user_agent + ja-JP: ユーザーエージェント + type: 1 + - field: status + i18n: + zh-CN: 状态 + en-US: status + ja-JP: ステータス + type: 1 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - handler + - path + - response_code + - method + - user_agent + - status + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - handler=handler + - path=path + - response_code=response_code + - method=method + - user_agent=user_agent + - status=status + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: '/metrics' + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH + method: GET + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: prometheus + + - name: storage_compactions_queued + i18n: + zh-CN: 正在排队的 TSM 数 + en-US: storage_compactions_queued + ja-JP: storage_compactions_queued + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: bucket + i18n: + zh-CN: 存储桶 + en-US: bucket + ja-JP: バケット + type: 1 + - field: engine + i18n: + zh-CN: 引擎类型 + en-US: engine + ja-JP: エンジン + type: 1 + - field: id + i18n: + zh-CN: 标识符 + en-US: id + ja-JP: id + type: 1 + - field: level + i18n: + zh-CN: 级别 + en-US: level + ja-JP: レベル + type: 1 + - field: path + i18n: + zh-CN: 数据文件路径 + en-US: path + ja-JP: パス + type: 1 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - bucket + - engine + - id + - level + - path + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - bucket=bucket + - engine=engine + - id=id + - level=level + - path=path + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: '/metrics' + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH + method: GET + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: prometheus + + - name: http_write_request_bytes + i18n: + zh-CN: HTTP写入请求的字节数量 + en-US: http_write_request_bytes + ja-JP: http書き込みリクエストのバイト + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: endpoint + i18n: + zh-CN: 终点 + en-US: endpoint + ja-JP: エンドポイント + type: 1 + - field: org_id + i18n: + zh-CN: 组织标识符 + en-US: org_id + ja-JP: org_id + type: 1 + - field: status + i18n: + zh-CN: 状态 + en-US: status + ja-JP: ステータス + type: 1 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - endpoint + - org_id + - status + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - endpoint=endpoint + - org_id=org_id + - status=status + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: '/metrics' + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH + method: GET + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: prometheus + + - name: qc_requests_total + i18n: + zh-CN: 质量控制请求总数 + en-US: qc_requests_total + ja-JP: qcリクエスト + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: result + i18n: + zh-CN: 结果 + en-US: result + ja-JP: 結果 + type: 1 + - field: org + i18n: + zh-CN: 组织标识符 + en-US: org + ja-JP: org + type: 1 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - result + - org + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - result=result + - org=org + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: '/metrics' + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH + method: GET + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: prometheus + + diff --git a/hertzbeat-manager/src/main/resources/define/app-influxdb_promql.yml b/hertzbeat-manager/src/main/resources/define/app-influxdb_promql.yml new file mode 100644 index 0000000..a6a3fb4 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-influxdb_promql.yml @@ -0,0 +1,443 @@ +# 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. + +# The monitoring type category: service-application service monitoring, db-database monitoring, custom-custom monitoring. +category: custom +# The monitoring application type name (consistent with the file name) +app: influxdb_promql +name: + zh-CN: InfluxDB-PromQL + en-US: InfluxDB-PromQL + ja-JP: InfluxDB-PromQL +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 Prometheus PromQL 从 Prometheus 服务器中查询到 InfluxDB 的通用指标数据来进行监控。此方案适用于 Prometheus 已监控 InfluxDB,需要从 Prometheus 服务器抓取 InfluxDB 的监控数据。<br>您可以点击 “<i>新建 InfluxDB-PromQL</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses Prometheus PromQL query metrics data from Prometheus Server to monitoring InfluxDB. This solution is suitable for Prometheus to monitor InfluxDB, and it need to capture InfluxDB monitoring data from the Prometheus server. <br>You could click the "<i>New InfluxDB-PromQL</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 Prometheus PromQL 從 Prometheus 服務器中查詢到 InfluxDB 的通用指標數據來進行監控。此方案適用于 Prometheus 已監控 InfluxDB,需要從 Prometheus 服務器抓取 InfluxDB 的監控數據。<br>您可以點擊 “<i>新建 InfluxDB-PromQL</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は Prometheus PromQL を介して Prometheus サーバーに InfluxDB の一般的なパフォーマンスのメトリクスをクエリして監視します。このシナリオは、PrometheusがすでにInfluxDBを監視しており、PrometheusサーバーからInfluxDBの監視データを取得する必要がある場合に適用されます。<br>「<i>新規 InfluxDB-PromQL</i>」をクリックして設定しましょう。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/influxdb_promql + en-US: https://hertzbeat.apache.org/docs/help/influxdb_promql +params: + - field: host + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + required: true + defaultValue: 9090 + - field: method + name: + zh-CN: 请求方式 + en-US: Method + ja-JP: リクエストメソッド + type: radio + required: true + options: + - label: GET请求 + value: GET + - label: POST请求 + value: POST + - label: PUT请求 + value: PUT + - label: DELETE请求 + value: DELETE + defaultValue: GET + - field: uri + name: + zh-CN: 相对路径 + en-US: URI + ja-JP: URI + type: text + limit: 200 + required: true + placeholder: 'API地址除IP端口外的路径 例如:/v2/book/bar' + defaultValue: /api/v1/query + - field: ssl + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL + type: boolean + required: false + - field: headers + name: + zh-CN: 请求Headers + en-US: Headers + ja-JP: ヘッダ + type: key-value + required: false + keyAlias: Header Name + valueAlias: Header Value + - field: params + name: + zh-CN: 查询Params + en-US: Params + ja-JP: パラメータ + type: key-value + required: false + keyAlias: Param Key + valueAlias: Param Value + - field: contentType + name: + zh-CN: Content-Type + en-US: Content-Type + ja-JP: コンテンツタイプ + type: text + placeholder: '请求BODY资源类型' + required: false + hide: true + - field: payload + name: + zh-CN: 请求BODY + en-US: BODY + ja-JP: ボディ + type: textarea + placeholder: 'POST PUT请求时有效' + required: false + hide: true + - field: authType + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + type: radio + required: false + hide: true + options: + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + hide: true + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + hide: true + +metrics: + # The first monitoring metrics basic_influxdb_memstats_alloc. + # Note: Built-in monitoring metrics include (responseTime - response time) + - name: basic_influxdb_memstats_alloc + i18n: + zh-CN: InfluxDB内存分配 + en-US: InfluxDB Memory Allocation + ja-JP: InfluxDBメモリの割り当て + # metrics scheduling priority(0->127), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # metrics fields list. + fields: + # Metric information includes the following: Field name, Type: 0-number, 1-string, instance: indicates whether the metric is the primary key, unit: the unit of the metric + - field: metric_instance + type: 1 + i18n: + zh-CN: 实例 + en-US: Instance + ja-JP: インスタンス + - field: timestamp + type: 1 + i18n: + zh-CN: 时间戳 + en-US: Timestamp + ja-JP: タイムスタンプ + - field: value + type: 1 + i18n: + zh-CN: 值 + en-US: Value + ja-JP: 値 + aliasFields: + - instance + - timestamp + - value + calculates: + - metric_instance=instance +# Monitoring protocol used for data collection, e.g. sql, ssh, http, telnet, wmi, snmp, sdk. + protocol: http +# When the protocol is HTTP, the specific collection configuration is as follows + http: + # Host: IPv4, IPv6 or domain name of the host. + host: ^_^host^_^ + # port + port: ^_^port^_^ + #URL request path. + url: ^_^uri^_^ + # request methods:GET POST PUT DELETE PATCH + method: ^_^method^_^ + # Whether to enable SSL/TLS, i.e. whether it is HTTP or HTTPS, default is false. + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + # Header content + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + # Request parameter content + params: + query: basic_influxdb_memstats_alloc + # authorization + authorization: + # Authentication method: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # Response data parsing method: default - system rules, jsonPath - jsonPath script, website - API availability monitoring. + # todo xmlPath-xmlPath script, PromQL-PromQL data rule + parseType: PromQL + + - name: influxdb_database_numMeasurements + i18n: + zh-CN: InfluxDB数据库测量值 + en-US: InfluxDB Database Measurements + ja-JP: InfluxDBデータベース測定 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # metrics fields list. + fields: + # Metric information includes the following: Field name, Type: 0-number, 1-string, instance: indicates whether the metric is the primary key, unit: the unit of the metric + - field: job + type: 1 + i18n: + zh-CN: 任务 + en-US: Job + ja-JP: タスク + - field: metric_instance + type: 1 + i18n: + zh-CN: 实例 + en-US: Instance + ja-JP: インスタンス + - field: database + type: 1 + i18n: + zh-CN: 数据库 + en-US: Database + ja-JP: データベース + - field: timestamp + type: 1 + i18n: + zh-CN: 时间戳 + en-US: Timestamp + ja-JP: タイムスタンプ + - field: value + type: 1 + i18n: + zh-CN: 值 + en-US: Value + ja-JP: 値 + aliasFields: + - job + - instance + - database + - timestamp + - value + calculates: + - metric_instance=instance + # Monitoring protocol used for data collection, e.g. sql, ssh, http, telnet, wmi, snmp, sdk. + protocol: http + # When the protocol is HTTP, the specific collection configuration is as follows + http: + # Host: IPv4, IPv6 or domain name of the host. + host: ^_^host^_^ + # port + port: ^_^port^_^ + #URL request path. + url: ^_^uri^_^ + # request methods:GET POST PUT DELETE PATCH + method: ^_^method^_^ + # Whether to enable SSL/TLS, i.e. whether it is HTTP or HTTPS, default is false. + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + # Header content + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + # Request parameter content + params: + query: influxdb_database_numMeasurements + # Authentication + authorization: + # Authentication method: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # Response data parsing method: default - system rules, jsonPath - jsonPath script, website - API availability monitoring. + # todo xmlPath-xmlPath script, PromQL-PromQL data rule + parseType: PromQL + + - name: influxdb_query_rate_seconds # Query rate per second + i18n: + zh-CN: 每秒查询速率 + en-US: Query Rate Per Second + ja-JP: QPS + # metrics scheduling priority(0->127), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # metrics fields list. + fields: + # Metric information includes the following: Field name, Type: 0-number, 1-string, instance: indicates whether the metric is the primary key, unit: the unit of the metric + - field: metric_instance + type: 1 + i18n: + zh-CN: 实例 + en-US: Instance + ja-JP: インスタンス + - field: timestamp + type: 1 + i18n: + zh-CN: 时间戳 + en-US: Timestamp + ja-JP: タイムスタンプ + - field: value + type: 1 + i18n: + zh-CN: 值 + en-US: Value + ja-JP: 値 + aliasFields: + - instance + - timestamp + - value + calculates: + - metric_instance=instance + # Monitoring protocol used for data collection, e.g. sql, ssh, http, telnet, wmi, snmp, sdk. + protocol: http + # When the protocol is HTTP, the specific collection configuration is as follows + http: + # Host: IPv4, IPv6 or domain name of the host. + host: ^_^host^_^ + # port + port: ^_^port^_^ + #URL request path. + url: ^_^uri^_^ + # request methods:GET POST PUT DELETE PATCH + method: ^_^method^_^ + # Whether to enable SSL/TLS, i.e. whether it is HTTP or HTTPS, default is false. + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + # Header content + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + # Request parameter content + params: + query: irate(influxdb_queryExecutor_queriesFinished[5m]) + # authorization + authorization: + # Authentication method: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # Response data parsing method: default - system rules, jsonPath - jsonPath script, website - API availability monitoring. + # todo xmlPath-xmlPath script, PromQL-PromQL data rule + parseType: PromQL + + - name: influxdb_queryExecutor_queriesFinished_10s # Query rate per second + + i18n: + zh-CN: 查询执行器每10秒查询完成数 + en-US: Query Executor Queries Finished Every 10 Seconds + ja-JP: クエリ実行10秒あたりのクエリ完了数 + # metrics scheduling priority(0->127), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + # metrics fields list. + fields: + # Metric information includes the following: Field name, Type: 0-number, 1-string, instance: indicates whether the metric is the primary key, unit: the unit of the metric + - field: metric_instance + type: 1 + i18n: + zh-CN: 实例 + en-US: Instance + ja-JP: インスタンス + - field: timestamp + type: 1 + i18n: + zh-CN: 时间戳 + en-US: Timestamp + ja-JP: タイムスタンプ + - field: value + type: 1 + i18n: + zh-CN: 值 + en-US: Value + ja-JP: 値 + aliasFields: + - instance + - timestamp + - value + calculates: + - metric_instance=instance + # Monitoring protocol used for data collection, e.g. sql, ssh, http, telnet, wmi, snmp, sdk. + protocol: http + # When the protocol is HTTP, the specific collection configuration is as follows + http: + # Host: IPv4, IPv6 or domain name of the host. + host: ^_^host^_^ + # port + port: ^_^port^_^ + #URL request path. + url: ^_^uri^_^ + # request methods:GET POST PUT DELETE PATCH + method: ^_^method^_^ + # Whether to enable SSL/TLS, i.e. whether it is HTTP or HTTPS, default is false. + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + # Header content + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + # Request parameter content + params: + query: influxdb_queryExecutor_queriesFinished[10s] + # authorization + authorization: + # Authentication method: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # Response data parsing method: default - system rules, jsonPath - jsonPath script, website - API availability monitoring. + # todo xmlPath-xmlPath script,PromQL-PromQL data rule + parseType: PromQL diff --git a/hertzbeat-manager/src/main/resources/define/app-iotdb.yml b/hertzbeat-manager/src/main/resources/define/app-iotdb.yml new file mode 100644 index 0000000..9e41790 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-iotdb.yml @@ -0,0 +1,387 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: iotdb +# The monitoring i18n name +name: + zh-CN: Apache IoTDB + en-US: Apache IoTDB + ja-JP: Apache IoTDB +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 Apache IoTDB JVM相关的物联网时序数据库的运行状态,内存任务集群等相关指标(cluster node status、jvm memory committed bytes、jvm memory used bytes、jvm threads states threads、quantity、cache hit、queue、hrift connections) 进行监控。<br><span class='help_module_span'>⚠️注意:您需要在 IoTDB 开启 prometheus exporter metrics 接口,<a class='help_module_content' href='https://iotdb.apache.org/zh/UserGuide/V0.13.x/Maintenance-Tools/Metric-Tool.html'>点击查看开启步骤</a>。</span> + en-US: HertzBeat monitoring the Apache IoTDB metrics such as operational status, memory, task, clusters, jvm etc. <br><span class='help_module_span'>⚠️Note:You should enable the prometheus metrics api in IoTDB. <a class='help_module_content' href='https://iotdb.apache.org/UserGuide/V0.13.x/Maintenance-Tools/Metric-Tool.html'>Click here to view the specific steps.</a></span> + zh-TW: HertzBeat 對 Apache IoTDB JVM相關的物聯網時序數據庫的運行狀態,內存任務集群等相關指標(cluster node status、jvm memory committed bytes、jvm memory used bytes、jvm threads states threads、quantity、cache hit、queue、hrift connections) 進行監控。<br><span class='help_module_span'>⚠️注意:您需要在 IoTDB 開啓 prometheus exporter metrics 接口,<a class='help_module_content' href='https://iotdb.apache.org/zh/UserGuide/V0.13.x/Maintenance-Tools/Metric-Tool.html'>點擊查看開啓步驟</a>。</span> + ja-JP: HertzBeat は Apache IoTDB Java仮想マシンについてのIoT時系列データベースを監視します。<br><span class='help_module_span'>⚠️注意:IoTDB で prometheus exporter metricsを有効にする必要があります。<a class='help_module_content' href='https://iotdb.apache.org/UserGuide/V0.13.x/Maintenance-Tools/Metric-Tool.html'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/iotdb/ + en-US: https://hertzbeat.apache.org/docs/help/iotdb/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # default value + defaultValue: 9091 + required: true + - field: timeout + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + type: number + required: false + # hide param-true or false + hide: true + defaultValue: 6000 + +# collect metrics config list +metrics: + # metrics - cluster_node_status + - name: cluster_node_status + i18n: + zh-CN: 集群节点状态 + en-US: Cluster Node Status + ja-JP: クラスタノードステータス + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: name + i18n: + zh-CN: 名称 + en-US: name + ja-JP: 名前 + type: 1 + label: true + - field: status + i18n: + zh-CN: 状态 + en-US: status + ja-JP: ステータス + type: 0 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - name + - value + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - name=name + - status=value + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: '/metrics' + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH + method: GET + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: prometheus + + - name: jvm_memory_committed_bytes + i18n: + zh-CN: JVM内存已提交 + en-US: JVM memory committed bytes + ja-JP: Java仮想マシンコミットされたメモリのバイト + priority: 1 + fields: + - field: area + i18n: + zh-CN: 区域 + en-US: Area + ja-JP: エリア + type: 1 + - field: id + i18n: + zh-CN: 内存块Id + en-US: Memory Block Id + ja-JP: メモリブロックId + type: 1 + label: true + - field: value + i18n: + zh-CN: JVM请求内存大小(当前) + en-US: JVM requests memory size (current) + ja-JP: Java仮想マシンが要求するメモリのサイズ + type: 0 + unit: MB + units: + - value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: jvm_memory_used_bytes + i18n: + zh-CN: JVM内存已使用 + en-US: JVM memory used bytes + ja-JP: Java仮想マシン使用したメモリのバイト + priority: 2 + fields: + - field: area + i18n: + zh-CN: 区域 + en-US: Area + ja-JP: エリア + type: 1 + - field: id + i18n: + zh-CN: 内存块Id + en-US: Memory Block Id + ja-JP: メモリブロックId + type: 1 + label: true + - field: value + i18n: + zh-CN: JVM已使用内存大小 + en-US: JVM used memory size + ja-JP: Java仮想マシン使用したメモリのサイズ + type: 0 + unit: MB + units: + - value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: jvm_threads_states_threads + i18n: + zh-CN: JVM线程状态 + en-US: JVM thread state + ja-JP: Java仮想マシンスレッド状態 + priority: 3 + fields: + - field: state + i18n: + zh-CN: 线程状态 + en-US: thread state + ja-JP: スレッド状態 + type: 1 + label: true + - field: count + i18n: + zh-CN: 线程数量 + en-US: thread count + ja-JP: スレッド総数 + type: 0 + aliasFields: + - state + - value + calculates: + - count=value + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: quantity + i18n: + zh-CN: 数量 + en-US: Quantity + ja-JP: 数量 + priority: 4 + fields: + - field: id + i18n: + zh-CN: Id + en-US: Id + ja-JP: Id + type: 1 + label: true + - field: name + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + type: 1 + - field: type + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: タイプ + type: 1 + - field: value + i18n: + zh-CN: 值 + en-US: Value + ja-JP: 値 + type: 0 + aliasFields: + - name + - type + - value + calculates: + - id=name+"-"+type + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: cache_hit + i18n: + zh-CN: 缓存命中率 + en-US: Cache hit rate + ja-JP: キャッシュ命中率 + priority: 5 + fields: + - field: name + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + type: 1 + label: true + - field: value + i18n: + zh-CN: 命中率 + en-US: Hit rate + ja-JP: 命中率 + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: queue + i18n: + zh-CN: 队列 + en-US: Queue + ja-JP: キュー + priority: 6 + fields: + - field: id + i18n: + zh-CN: ID + en-US: ID + ja-JP: ID + type: 1 + label: true + - field: name + i18n: + zh-CN: 队列名称 + en-US: Queue name + ja-JP: キュー名 + type: 1 + - field: status + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + type: 1 + - field: value + i18n: + zh-CN: 当前队列任务数量 + en-US: Number of tasks in queue + ja-JP: キューのタスク数 + type: 0 + aliasFields: + - name + - status + - value + calculates: + - id=name+"-"+status + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: thrift_connections + i18n: + zh-CN: Thrift连接数 + en-US: Thrift connection number + ja-JP: Thrift接続数 + priority: 7 + fields: + - field: name + i18n: + zh-CN: 连接名称 + en-US: Connection name + ja-JP: 接続名 + type: 1 + label: true + - field: connection + i18n: + zh-CN: 连接数 + en-US: Connection number + ja-JP: 接続数 + type: 0 + aliasFields: + - name + - value + calculates: + - connection=value + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus diff --git a/hertzbeat-manager/src/main/resources/define/app-ipmi.yml b/hertzbeat-manager/src/main/resources/define/app-ipmi.yml new file mode 100644 index 0000000..f6c2051 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-ipmi.yml @@ -0,0 +1,216 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: server +# The monitoring type eg: linux windows tomcat mysql aws... +app: ipmi +# The monitoring i18n name +name: + zh-CN: IPMI + en-US: IPMI + ja-JP: IPMI +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对支持 IPMI 服务的服务器进行测量监控。<br>您可以点击 “<i>新建 IPMI</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitoring servers supporting IPMI services. You could click the "<i>New IPMI</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對支援 IPMI 服務的伺服器進行測量監控。<br>您可以點擊“<i>IPMI</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat はIPMIサービスをサポートするサーバを監視します。<br>「<i>新規 IPMI</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/ipmi + en-US: https://hertzbeat.apache.org/docs/help/ipmi +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 623 + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false +# collect metrics config list +metrics: + # metrics - cpu + - name: Chassis + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: system_power + type: 1 + i18n: + zh-CN: 系统电源状态 + en-US: System Power + ja-JP: システム電源 + - field: power_overload + type: 1 + i18n: + zh-CN: 电源过载 + en-US: Power Overload + ja-JP: オーバーロード + - field: power_interlock + type: 1 + i18n: + zh-CN: 电源互锁 + en-US: Power Interlock + ja-JP: インターロック + - field: power_fault + type: 1 + i18n: + zh-CN: 主电源故障 + en-US: Main Power Fault + ja-JP: 主電源故障 + - field: power_control_fault + type: 1 + i18n: + zh-CN: 电源控制故障 + en-US: Power Control Fault + ja-JP: 電源制御故障 + - field: power_restore_policy + type: 1 + i18n: + zh-CN: 电源恢复策略 + en-US: Power Restore Policy + ja-JP: 電源復旧ポリシー + - field: last_power_event + type: 1 + i18n: + zh-CN: 最后一次电源事件 + en-US: Last Power Event + ja-JP: 最後の電源イベント + - field: fan_fault + type: 1 + i18n: + zh-CN: 风扇故障 + en-US: Cooling/Fan Fault + ja-JP: ファン故障 + - field: drive_fault + type: 1 + i18n: + zh-CN: 硬盘故障 + en-US: Drive Fault + ja-JP: ドライブ故障 + - field: front_panel_lockout_active + type: 1 + i18n: + zh-CN: 前面板锁定 + en-US: Front-Panel Lockout + ja-JP: フロントパネルのロックアウト + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ipmi + # the config content when protocol is ipmi + ipmi: + # ipmi host: ipv4 ipv6 domain + host: ^_^host^_^ + # ipmi port + port: ^_^port^_^ + # ipmi username + username: ^_^username^_^ + # ipmi password + password: ^_^password^_^ + # ipmi command type: Chassis | Sensor + type: Chassis + - name: Sensor + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: sensor_id + type: 1 + i18n: + zh-CN: 传感器标识 + en-US: Sensor ID + ja-JP: センサーID + - field: entity_id + type: 1 + i18n: + zh-CN: 实体标识 + en-US: Entity ID + ja-JP: エンティティID + - field: sensor_type + type: 1 + i18n: + zh-CN: 传感器类型 + en-US: Sensor Type + ja-JP: センサータイプ + - field: sensor_reading + type: 1 + i18n: + zh-CN: 传感器读数 + en-US: Sensor Reading + ja-JP: センサーの読み取り値 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ipmi + # the config content when protocol is ipmi + ipmi: + # ipmi host: ipv4 ipv6 domain + host: ^_^host^_^ + # ipmi port + port: ^_^port^_^ + # ipmi username + username: ^_^username^_^ + # ipmi password + password: ^_^password^_^ + # ipmi command type: Chassis | Sensor + type: Sensor diff --git a/hertzbeat-manager/src/main/resources/define/app-jenkins.yml b/hertzbeat-manager/src/main/resources/define/app-jenkins.yml new file mode 100644 index 0000000..ebb8570 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-jenkins.yml @@ -0,0 +1,691 @@ +# 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. + +# The monitoring type category: service-application service monitoring, db-database monitoring, custom-custom monitoring. +category: CI/CD +# The monitoring application type name (consistent with the file name) +app: jenkins +# The monitoring i18n name +name: + zh-CN: Jenkins + en-US: Jenkins +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 Jenkins 通用指标进行测量监控。<br>您可以点击 “<i>新建 Jenkins</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat monitors Jenkins generic metrics. You can click "<i>New Jenkins</i>" to configure it, or select "<i>More Actions</i>" to import an existing configuration. +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/jenkins + en-US: https://hertzbeat.apache.org/docs/help/jenkins +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + required: true + defaultValue: 8080 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + type: number + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 + +# collect metrics config list +metrics: + # Basic information indicators + - name: jenkins_health_check_score + i18n: + zh-CN: Jenkins 健康检查分值 + en-US: Jenkins health check score + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: metric_value + type: 0 + i18n: + zh-CN: 分值 + en-US: score + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /prometheus + # http method: GET POST PUT DELETE PATCH + method: GET + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: prometheus + + - name: default_jenkins_version_info + i18n: + zh-CN: Jenkins 版本信息 + en-US: Jenkins version info + priority: 1 + fields: + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: version + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: default_jenkins_up + i18n: + zh-CN: Jenkins 是否存活 + en-US: Jenkins up + priority: 2 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: up + en-US: up + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: default_jenkins_uptime + i18n: + zh-CN: Jenkins 运行时间 + en-US: Jenkins uptime + priority: 3 + fields: + - field: metric_value + type: 0 + unit: 'ms' + i18n: + zh-CN: 运行时间 + en-US: uptime + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: vm_uptime_milliseconds + i18n: + zh-CN: JVM 运行时间 + en-US: JVM uptime + priority: 4 + fields: + - field: metric_value + type: 0 + unit: 'ms' + i18n: + zh-CN: 运行时间 + en-US: uptime + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: system_cpu_load + i18n: + zh-CN: 系统负载 + en-US: System load + priority: 5 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: value + en-US: value + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: vm_count + i18n: + zh-CN: JVM 线程总数 + en-US: Total JVM threads + priority: 6 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: 总数 + en-US: total + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: vm_memory_heap_max + i18n: + zh-CN: JVM 可使用的最大内存限制 + en-US: JVM max heap memory + priority: 7 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: 大小 + en-US: size + units: + - metric_value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: vm_memory_heap_used + i18n: + zh-CN: JVM 当前使用的内存大小 + en-US: JVM heap memory used + priority: 8 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: 大小 + en-US: size + units: + - metric_value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: jenkins_project_count_value + i18n: + zh-CN: Jenkins 项目数量 + en-US: Jenkins project count + priority: 9 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: 数量 + en-US: quantity + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: jenkins_project_enabled_count_value + i18n: + zh-CN: 已启用的项目数量 + en-US: Enabled project count + priority: 10 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: 数量 + en-US: quantity + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: jenkins_queue_size_value + i18n: + zh-CN: 构建队列中的任务数量 + en-US: Build queue size + priority: 11 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: 数量 + en-US: quantity + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: jenkins_node_online_value + i18n: + zh-CN: 当前在线的构建节点数量 + en-US: Online build nodes count + priority: 12 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: 数量 + en-US: quantity + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: jenkins_job_count_value + i18n: + zh-CN: 作业数量 + en-US: Job count + priority: 13 + fields: + - field: metric_value + type: 0 + i18n: + zh-CN: 数量 + en-US: quantity + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: default_jenkins_executors_available + i18n: + zh-CN: 可用的执行器数量 + en-US: Available executors count + priority: 14 + fields: + - field: label + type: 1 + label: true + i18n: + zh-CN: 标签 + en-US: label + - field: metric_value + type: 0 + i18n: + zh-CN: 数量 + en-US: quantity + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: default_jenkins_executors_busy + i18n: + zh-CN: 忙碌的执行器数量 + en-US: Busy executors count + priority: 15 + fields: + - field: label + type: 1 + label: true + i18n: + zh-CN: 标签 + en-US: label + - field: metric_value + type: 0 + i18n: + zh-CN: 数量 + en-US: quantity + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: default_jenkins_executors_connecting + i18n: + zh-CN: 正在连接的执行器数量 + en-US: Connecting executors count + priority: 16 + fields: + - field: label + type: 1 + label: true + i18n: + zh-CN: 标签 + en-US: label + - field: metric_value + type: 0 + i18n: + zh-CN: 数量 + en-US: quantity + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: default_jenkins_builds_duration_milliseconds_summary_sum + i18n: + zh-CN: 任务构建时长汇总(毫秒) + en-US: Job build duration sum (ms) + priority: 17 + fields: + - field: jenkins_job + label: true + i18n: + zh-CN: 任务 + en-US: job + type: 1 + - field: repo + label: true + i18n: + zh-CN: 仓库 + en-US: repo + type: 1 + - field: buildable + label: true + i18n: + zh-CN: 是否可构建 + en-US: buildable + type: 1 + - field: metric_value + unit: 'ms' + i18n: + zh-CN: value + en-US: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: default_jenkins_builds_last_build_duration_milliseconds + i18n: + zh-CN: 最近一次构建的构建时间(毫秒) + en-US: Last build time (ms) + priority: 18 + fields: + - field: jenkins_job + label: true + i18n: + zh-CN: 任务 + en-US: job + type: 1 + - field: repo + label: true + i18n: + zh-CN: 仓库 + en-US: repo + type: 1 + - field: buildable + label: true + i18n: + zh-CN: 是否可构建 + en-US: buildable + type: 1 + - field: metric_value + unit: 'ms' + i18n: + zh-CN: value + en-US: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: default_jenkins_builds_success_build_count_total + i18n: + zh-CN: 构建成功次数 + en-US: Successful build count + priority: 19 + fields: + - field: jenkins_job + label: true + i18n: + zh-CN: 任务 + en-US: job + type: 1 + - field: repo + label: true + i18n: + zh-CN: 仓库 + en-US: repo + type: 1 + - field: buildable + label: true + i18n: + zh-CN: 是否可构建 + en-US: buildable + type: 1 + - field: metric_value + i18n: + zh-CN: 总数 + en-US: total + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: default_jenkins_builds_failed_build_count_total + i18n: + zh-CN: 构建失败次数 + en-US: Failed build count + priority: 20 + fields: + - field: jenkins_job + label: true + i18n: + zh-CN: 任务 + en-US: job + type: 1 + - field: repo + label: true + i18n: + zh-CN: 仓库 + en-US: repo + type: 1 + - field: buildable + label: true + i18n: + zh-CN: 是否可构建 + en-US: buildable + type: 1 + - field: metric_value + i18n: + zh-CN: 总数 + en-US: total + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: default_jenkins_builds_unstable_build_count_total + i18n: + zh-CN: 不稳定构建次数 + en-US: Unstable build count + priority: 21 + fields: + - field: jenkins_job + label: true + i18n: + zh-CN: 任务 + en-US: job + type: 1 + - field: repo + label: true + i18n: + zh-CN: 仓库 + en-US: repo + type: 1 + - field: buildable + label: true + i18n: + zh-CN: 是否可构建 + en-US: buildable + type: 1 + - field: metric_value + i18n: + zh-CN: 总数 + en-US: total + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: default_jenkins_builds_total_build_count_total + i18n: + zh-CN: 总构建次数(不包括未构建状态) + en-US: Total builds count (excluding not built) + priority: 22 + fields: + - field: jenkins_job + label: true + i18n: + zh-CN: 任务 + en-US: job + type: 1 + - field: repo + label: true + i18n: + zh-CN: 仓库 + en-US: repo + type: 1 + - field: buildable + label: true + i18n: + zh-CN: 是否可构建 + en-US: buildable + type: 1 + - field: metric_value + i18n: + zh-CN: 总数 + en-US: total + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus + + - name: default_jenkins_builds_last_build_result_ordinal + i18n: + zh-CN: 任务构建状态(最近一次构建) + en-US: Last build status + priority: 23 + fields: + - field: jenkins_job + label: true + i18n: + zh-CN: 任务 + en-US: job + type: 1 + - field: repo + label: true + i18n: + zh-CN: 仓库 + en-US: repo + type: 1 + - field: buildable + label: true + i18n: + zh-CN: 是否可构建 + en-US: buildable + type: 1 + - field: metric_value + i18n: + zh-CN: 状态 + en-US: status + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /prometheus + method: GET + parseType: prometheus diff --git a/hertzbeat-manager/src/main/resources/define/app-jetty.yml b/hertzbeat-manager/src/main/resources/define/app-jetty.yml new file mode 100644 index 0000000..d916d19 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-jetty.yml @@ -0,0 +1,429 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: webserver +# The monitoring type eg: linux windows tomcat mysql aws... +app: jetty +# The monitoring i18n name +name: + zh-CN: Jetty应用服务器 + en-US: Jetty AppServer + ja-JP: Jetty応用サーバー +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-jmx'>JMX 协议</a> 对 Jetty 应用服务器的通用性能指标(内存信息,类加载,线程状态,JVM等)进行采集监控。<br><span class='help_module_span'>注意⚠️:您需要在 Jetty 应用开启 JMX 服务, <a class='help_module_content' href='https://eclipse.dev/jetty/documentation/jetty-10/operations-guide/index.html#og-jmx-remote'>点击查看开启步骤</a>。</span> + en-US: HertzBeat monitoring general performance metrics(memory pool, class loading, thread, jvm etc) of Jetty application server through <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'>JMX protocol</a>. <br><span class='help_module_span'>Note⚠️:You should enable the JMX service in Jetty application. <a class='help_module_content' href='https://eclipse.dev/jetty/documentation/jetty-10/operations-guide/index.html#og-jmx-remote'>Click here to view the specific steps.</a></span> + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/advanced/extend-jmx'>JMX 協議</a> 對 Jetty 應用服務器的通用性能指標(內存信息,類加載,線程狀態,JVM等)進行采集監控。<br><span class='help_module_span'>注意⚠️:您需要在 Jetty 應用開啓 JMX 服務, <a class='help_module_content' href='https://eclipse.dev/jetty/documentation/jetty-10/operations-guide/index.html#og-jmx-remote'>點擊查看開啓步驟</a>。</span> + ja-JP: HertzBeatは <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMXプロトコルを介して</a> Jetty応用サーバーの一般的なパフォーマンスのメトリクスを監視します。<br><span class='help_module_span'> ⚠️注意:Jetty応用 で JMX サービスを有効にする必要があります。<a class='help_module_content' href='https://eclipse.dev/jetty/documentation/jetty-10/operations-guide/index.html#og-jmx-remote'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/jetty/ + en-US: https://hertzbeat.apache.org/docs/help/jetty/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: JMX端口 + en-US: JMX Port + ja-JP: JMXポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # port default value + defaultValue: 1099 + # field-param field key + - field: url + # name-param field display i18n name + name: + zh-CN: JMX URL + en-US: JMX URL + ja-JP: JMX URL + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + # param field input placeholder + placeholder: 'service:jmx:rmi:///jndi/rmi://host:port/jmxrmi' + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 虚拟机基础信息 + en-US: JVM Basic + ja-JP: Java仮想マシン基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # monitor metric content in metrics + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: VmName + type: 1 + i18n: + zh-CN: 名称 + en-US: Vm Name + ja-JP: 仮想マシン名 + - field: VmVendor + type: 1 + i18n: + zh-CN: 厂商 + en-US: Vm Vendor + ja-JP: 仮想マシンベンダー + - field: VmVersion + type: 1 + i18n: + zh-CN: 版本 + en-US: Vm Version + ja-JP: 仮想マシンバージョン + - field: Uptime + type: 0 + unit: ms + i18n: + zh-CN: 运行时长 + en-US: Up time + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jmx + # the config content when protocol is http + jmx: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: java.lang:type=Runtime + url: ^_^url^_^ + # metrics - server + - name: server + i18n: + zh-CN: Jetty Server + en-US: Jetty Server + ja-JP: Jetty サーバー + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # monitor metric content in metrics + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Vm Version + ja-JP: 仮想マシンバージョン + - field: state + type: 1 + i18n: + zh-CN: 运行状态 + en-US: Run State + ja-JP: 実行状態 + - field: startupTime + type: 0 + i18n: + zh-CN: 启动时间 + en-US: Startup Time + ja-JP: 起動時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jmx + # the config content when protocol is http + jmx: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: org.eclipse.jetty.server:type=server,id=0 + url: ^_^url^_^ + # metrics - server + - name: memory_pool + i18n: + zh-CN: 内存池 + en-US: Memory Pool + ja-JP: メモリプール + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # monitor metric content in metrics + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: name + type: 1 + label: true + i18n: + zh-CN: 指标名称 + en-US: Name + ja-JP: メトリクス名 + - field: committed + type: 0 + i18n: + zh-CN: 已分配内存 + en-US: Committed + ja-JP: コミットされたメモリ + - field: init + type: 0 + i18n: + zh-CN: 初始化内存 + en-US: Init + ja-JP: イニシャルメモリ + - field: max + type: 0 + i18n: + zh-CN: 最大内存 + en-US: Max + ja-JP: 最大メモリ + - field: used + type: 0 + i18n: + zh-CN: 已使用内存 + en-US: Used + ja-JP: 使用したメモリ + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - Name + - Usage->committed + - Usage->init + - Usage->max + - Usage->used + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - name=Name + - committed=Usage->committed + - init=Usage->init + - max=Usage->max + - used=Usage->used + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jmx + # the config content when protocol is http + jmx: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=MemoryPool,name=* + url: ^_^url^_^ + # metrics - class_loading + - name: class_loading + i18n: + zh-CN: 类加载信息 + en-US: Class Loading + ja-JP: クラスローディング情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + # monitor metric content in metrics + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: LoadedClassCount + type: 0 + i18n: + zh-CN: 已加载类总数 + en-US: Loaded Class Count + ja-JP: ロードされたクラス数 + - field: TotalLoadedClassCount + type: 0 + i18n: + zh-CN: 总加载类总数 + en-US: Total Loaded Class Count + ja-JP: ロードされたクラス総数 + - field: UnloadedClassCount + type: 0 + i18n: + zh-CN: 未加载类总数 + en-US: Unloaded Class Count + ja-JP: アンロードされたクラス総数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jmx + # the config content when protocol is http + jmx: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=ClassLoading + url: ^_^url^_^ + # metrics - thread + - name: thread + i18n: + zh-CN: 线程信息 + en-US: Thread + ja-JP: スレッド情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 4 + # monitor metric content in metrics + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: TotalStartedThreadCount + type: 0 + i18n: + zh-CN: 已启动线程总数 + en-US: Total Started Thread Count + ja-JP: スレッド総数 + - field: ThreadCount + type: 0 + i18n: + zh-CN: 活跃线程数 + en-US: Thread Count + ja-JP: 活躍スレッド数 + - field: PeakThreadCount + type: 0 + i18n: + zh-CN: 最大峰值线程数 + en-US: Peak Thread Count + ja-JP: 最大スレッド数 + - field: DaemonThreadCount + type: 0 + i18n: + zh-CN: 活跃守护线程数 + en-US: Daemon Thread Count + ja-JP: デーモンスレッド数 + - field: CurrentThreadUserTime + type: 0 + unit: s + i18n: + zh-CN: 线程占用的CPU时间(用户态) + en-US: Current Thread User Time + ja-JP: 現在のスレッドユーザー時間 + - field: CurrentThreadCpuTime + type: 0 + unit: s + i18n: + zh-CN: 线程占用的CPU时间 + en-US: Current Thread CPU Time + ja-JP: 現在のスレッドシステム時間 + units: + - CurrentThreadUserTime=NS->S + - CurrentThreadCpuTime=NS->S + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jmx + # the config content when protocol is http + jmx: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=Threading + url: ^_^url^_^ + # metrics - webapp + - name: webapp + i18n: + zh-CN: Web应用 + en-US: Webapp + ja-JP: Web応用 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 5 + # monitor metric content in metrics + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: displayName + type: 1 + label: true + i18n: + zh-CN: 应用名称 + en-US: App Name + ja-JP: 応用名 + - field: contextPath + type: 1 + i18n: + zh-CN: 上下文路径 + en-US: Context Path + ja-JP: コンテキストパス + - field: state + type: 1 + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + - field: resourceBase + type: 1 + i18n: + zh-CN: 资源 + en-US: Resource Base + ja-JP: リソース + - field: shutdown + type : 1 + i18n: + zh-CN: 是否关闭 + en-US: Shutdown + ja-JP: シャットダウン + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jmx + # the config content when protocol is http + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: org.eclipse.jetty.webapp:context=*,type=webappcontext,id=0 + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-jvm.yml b/hertzbeat-manager/src/main/resources/define/app-jvm.yml new file mode 100644 index 0000000..bc2515d --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-jvm.yml @@ -0,0 +1,376 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: program +# The monitoring type eg: linux windows tomcat mysql aws... +app: jvm +# The monitoring i18n name +name: + zh-CN: JVM虚拟机 + en-US: JVM + ja-JP: Java仮想マシン +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a href="https://hertzbeat.apache.org/docs/advanced/extend-jmx">JMX 协议</a> 对 JVM 虚拟机的通用性能指标(基础信息,内存池,类加载,线程信息等)进行采集监控。<br>⚠️注意:您需要在 JVM 应用中开启 JMX 服务,应用启动时添加 JMX 参数, 可自定义暴露端口,对外IP。<a href="https://docs.oracle.com/javase/1.5.0/docs/guide/management/agent.html#remote">点击查看开启步骤</a>。 + en-US: HertzBeat uses <a href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'>JMX Protocol</a> to monitoring and collect general performance metric of jvm application. <br>⚠️Note:You need to enable JMX services in JVM application, and add the JXM parameters when the application start. You can also customize external IP address and exposed port.<a href='https://docs.oracle.com/javase/1.5.0/docs/guide/management/agent.html#remote'>Click here to view the activation steps.</a> + zh-TW: HertzBeat 使用 <a href="https://hertzbeat.apache.org/docs/advanced/extend-jmx">JMX 協議</a> 對 JVM 虛擬機的通用性能指標(基礎信息,內存池,類加載,線程信息等)進行采集監控。<br>⚠️注意:您需要在 JVM 應用中開啓 JMX 服務,應用啓動時添加 JMX 參數, 可自定義暴露端口,對外IP。<a href="https://docs.oracle.com/javase/1.5.0/docs/guide/management/agent.html#remote">點擊查看開啓步驟</a>。 + ja-JP: HertzBeat は <a href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMXプロトコルを介して</a> Java仮想マシンの一般的なパフォーマンスのメトリクスを監視します。<br>⚠️注意:Java仮想マシンの応用 で JMX サービスを有効にする必要があります。<a href='https://docs.oracle.com/javase/1.5.0/docs/guide/management/agent.html#remote'>クリックしてガイドを見ます</a>。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/jvm/ + en-US: https://hertzbeat.apache.org/docs/help/jvm/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 9999 + # field-param field key + - field: url + # name-param field display i18n name + name: + zh-CN: JMX URL + en-US: JMX URL + ja-JP: JMX URL + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + # param field input placeholder + placeholder: 'service:jmx:rmi:///jndi/rmi://host:port/jmxrmi' + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic + - name: basic + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + i18n: + zh-CN: 虚拟机基础信息 + en-US: JVM Basic + ja-JP: Java仮想マシン基礎情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: VmName + type: 1 + i18n: + zh-CN: 名称 + en-US: Vm Name + ja-JP: 仮想マシン名 + - field: VmVendor + type: 1 + i18n: + zh-CN: 厂商 + en-US: Vm Vendor + ja-JP: 仮想マシンベンダー + - field: VmVersion + type: 1 + i18n: + zh-CN: 版本 + en-US: Vm Version + ja-JP: 仮想マシンバージョン + - field: Uptime + type: 0 + unit: ms + i18n: + zh-CN: 运行时长 + en-US: Up time + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jmx + # the config content when protocol is jmx + jmx: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: java.lang:type=Runtime + url: ^_^url^_^ + + - name: memory_pool + priority: 1 + i18n: + zh-CN: 内存池 + en-US: Memory Pool + ja-JP: メモリプール + fields: + - field: name + type: 1 + label: true + i18n: + zh-CN: 指标名称 + en-US: Name + ja-JP: メトリクス名 + - field: committed + type: 0 + unit: MB + i18n: + zh-CN: 已分配内存 + en-US: Committed + ja-JP: コミットされたメモリ + - field: init + type: 0 + unit: MB + i18n: + zh-CN: 初始化内存 + en-US: Init + ja-JP: イニシャルメモリ + - field: max + type: 0 + unit: MB + i18n: + zh-CN: 最大内存 + en-US: Max + ja-JP: 最大メモリ + - field: used + type: 0 + unit: MB + i18n: + zh-CN: 已使用内存 + en-US: Used + ja-JP: 使用したメモリ + units: + - committed=B->MB + - init=B->MB + - max=B->MB + - used=B->MB + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - Name + - Usage->committed + - Usage->init + - Usage->max + - Usage->used + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - name=Name + - committed=Usage->committed + - init=Usage->init + - max=Usage->max + - used=Usage->used + protocol: jmx + jmx: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=MemoryPool,name=* + url: ^_^url^_^ + + - name: code_cache + priority: 2 + i18n: + zh-CN: 本地代码缓冲区 + en-US: Code Cache + ja-JP: コードキャッシュ + fields: + - field: committed + type: 0 + i18n: + zh-CN: 已分配内存 + en-US: Committed + ja-JP: コミットされたメモリ + - field: init + type: 0 + i18n: + zh-CN: 初始化内存 + en-US: Init + ja-JP: イニシャルメモリ + - field: max + type: 0 + i18n: + zh-CN: 最大内存 + en-US: Max + ja-JP: 最大メモリ + - field: used + type: 0 + i18n: + zh-CN: 已使用内存 + en-US: Used + ja-JP: 使用したメモリ + aliasFields: + - Usage->committed + - Usage->init + - Usage->max + - Usage->used + calculates: + - committed=Usage->committed + - init=Usage->init + - max=Usage->max + - used=Usage->used + protocol: jmx + jmx: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=MemoryPool,name=Code Cache + url: ^_^url^_^ + + - name: class_loading + priority: 3 + i18n: + zh-CN: 类加载信息 + en-US: Class Loading + ja-JP: クラスローディング情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: LoadedClassCount + type: 0 + i18n: + zh-CN: 当前已加载类数量 + en-US: Loaded Class Count + ja-JP: ロードされたクラス数 + - field: TotalLoadedClassCount + type: 0 + i18n: + zh-CN: 已加载类总数量 + en-US: Total Loaded Class Count + ja-JP: ロードされたクラス総数 + - field: UnloadedClassCount + type: 0 + i18n: + zh-CN: 未加载类总数量 + en-US: Unloaded Class Count + ja-JP: アンロードされたクラス総数 + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=ClassLoading + url: ^_^url^_^ + + - name: thread + priority: 4 + i18n: + zh-CN: 线程信息 + en-US: Thread + ja-JP: スレッド情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: TotalStartedThreadCount + type: 0 + i18n: + zh-CN: 已启动线程总数 + en-US: Total Started Thread Count + ja-JP: スレッド総数 + - field: ThreadCount + type: 0 + i18n: + zh-CN: 活跃线程数 + en-US: Thread Count + ja-JP: 活躍スレッド数 + - field: PeakThreadCount + type: 0 + i18n: + zh-CN: 最大峰值线程数 + en-US: Peak Thread Count + ja-JP: 最大スレッド数 + - field: DaemonThreadCount + type: 0 + i18n: + zh-CN: 活跃守护线程数 + en-US: Daemon Thread Count + ja-JP: デーモンスレッド数 + - field: CurrentThreadUserTime + type: 0 + unit: s + i18n: + zh-CN: 线程占用的CPU时间(用户态) + en-US: Current Thread User Time + ja-JP: 現在のスレッドユーザー時間 + - field: CurrentThreadCpuTime + type: 0 + unit: s + i18n: + zh-CN: 线程占用的CPU时间 + en-US: Current Thread CPU Time + ja-JP: 現在のスレッドシステム時間 + units: + - CurrentThreadUserTime=NS->S + - CurrentThreadCpuTime=NS->S + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=Threading + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-kafka.yml b/hertzbeat-manager/src/main/resources/define/app-kafka.yml new file mode 100644 index 0000000..3480d1a --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-kafka.yml @@ -0,0 +1,970 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: mid +# The monitoring type eg: linux windows tomcat mysql aws... +app: kafka +# The monitoring i18n name +name: + zh-CN: Kafka消息系统 + en-US: Kafka Message + ja-JP: Kafkaメッセージングシステム +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a href="https://hertzbeat.apache.org/docs/advanced/extend-jmx">JMX 协议</a> 对 Kafka 的通用性能指标 (server info、code cache、active controller count、broker partition count、broker leader count、broker handler avg percent etc) 进行采集监控。<br><span class='help_module_span'>注意⚠️:您需要在 Kafka 开启 JMX 服务,应用启动时添加 JMX 参数,暴露端口,对外IP。下方配置的端口即为JMX暴露的端口,而非Kafka的server端口。<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/kafka'>点击查看开启步骤</a>。</span> + en-US: HertzBeat uses <a href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'>JMX Protocol</a> to monitoring kafka general performance metrics (server info、code cache、active controller count、broker partition count、broker leader count、broker handler avg percent etc). <br><span class='help_module_span'>Note⚠️:You need to enable JMX service in Kafka, export JMX port and config params.The port configured below is the JMX exposed port, not the Kafka server port. <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/kafka'>Click here to view the specific steps.</a></span> + zh-TW: HertzBeat 使用 <a href="https://hertzbeat.apache.org/docs/advanced/extend-jmx">JMX 協議</a> 對 Kafka 的通用性能指標 (server info、code cache、active controller count、broker partition count、broker leader count、broker handler avg percent etc) 進行采集監控。<br><span class='help_module_span'>注意⚠️:您需要在 Kafka 開啓 JMX 服務,應用啓動時添加 JMX 參數,暴露端口,對外IP。下方配置的端口即為 JMX 暴露的端口,而非 Kafka 的伺服器端口。<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/kafka'>點擊查看開啓步驟</a>。</span> + ja-JP: HertzBeat は <a href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMXプロトコルを介して</a> Kafkaの一般的なパフォーマンスのメトリクスを監視します。<br><span class='help_module_span'>⚠️注意:Kafka で JMX サービスを有効にする必要があります。<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/kafka'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/kafka + en-US: https://hertzbeat.apache.org/docs/help/kafka +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + # when type is number, range is required + range: '[0,65535]' + required: true + defaultValue: 9999 + - field: url + name: + zh-CN: JMX URL + en-US: JMX URL + ja-JP: JMX URL + type: text + required: false + hide: true + placeholder: 'service:jmx:rmi:///jndi/rmi://host:port/jmxrmi' + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + hide: true + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + hide: true +# collect metrics config list +metrics: + # metrics - server_info + - name: server_info + i18n: + zh-CN: 服务器信息 + en-US: Server Info + ja-JP: サーバー情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: Version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + - field: StartTimeMs + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Start Time + ja-JP: 起動時間 + - field: CommitId + type: 1 + i18n: + zh-CN: CommitId + en-US: CommitId + ja-JP: CommitId + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jmx + # the config content when protocol is jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: kafka.server:type=app-info,id=* + # jmx url + url: ^_^url^_^ + + - name: jvm_basic + i18n: + zh-CN: 虚拟机基础信息 + en-US: JVM Basic + ja-JP: Java仮想マシン基礎情報 + priority: 1 + fields: + - field: VmName + type: 1 + i18n: + zh-CN: 名称 + en-US: Vm Name + ja-JP: 仮想マシン名 + - field: VmVendor + type: 1 + i18n: + zh-CN: 厂商 + en-US: Vm Vendor + ja-JP: 仮想マシンベンダー + - field: VmVersion + type: 1 + i18n: + zh-CN: 版本 + en-US: Vm Version + ja-JP: 仮想マシンバージョン + - field: Uptime + type: 0 + unit: ms + i18n: + zh-CN: 运行时长 + en-US: Up time + ja-JP: アップタイム + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: java.lang:type=Runtime + url: ^_^url^_^ + + - name: jvm_memory_pool + i18n: + zh-CN: 内存池 + en-US: Memory Pool + ja-JP: メモリプール + priority: 2 + fields: + - field: name + type: 1 + label: true + i18n: + zh-CN: 指标名称 + en-US: Name + ja-JP: メトリクス名 + - field: committed + type: 0 + i18n: + zh-CN: 已分配内存 + en-US: Committed + ja-JP: コミットされたメモリ + - field: init + type: 0 + i18n: + zh-CN: 初始化内存 + en-US: Init + ja-JP: イニシャルメモリ + - field: max + type: 0 + i18n: + zh-CN: 最大内存 + en-US: Max + ja-JP: 最大メモリ + - field: used + type: 0 + i18n: + zh-CN: 已使用内存 + en-US: Used + ja-JP: 使用したメモリ + aliasFields: + - Name + - Usage->committed + - Usage->init + - Usage->max + - Usage->used + calculates: + - name=Name + - committed=Usage->committed + - init=Usage->init + - max=Usage->max + - used=Usage->used + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=MemoryPool,name=* + url: ^_^url^_^ + + - name: kafka_controller_metrics + i18n: + zh-CN: Kafka控制器指标 + en-US: Kafka Controller Metrics + ja-JP: Kafkaコントローラーのメトリクス + priority: 3 + fields: + - field: ActiveBrokerCount + type: 0 + i18n: + zh-CN: 活跃代理数量 + en-US: Active Broker Count + ja-JP: 活動中のブローカー数 + - field: ActiveControllerCount + type: 0 + i18n: + zh-CN: 活跃控制器数量 + en-US: Active Controller Count + ja-JP: 活動中のコントローラー数 + - field: ControllerState + type: 0 + i18n: + zh-CN: 控制器状态 + en-US: Controller State + ja-JP: コントローラー状態 + - field: FencedBrokerCount + type: 0 + i18n: + zh-CN: 被隔离代理数量 + en-US: Fenced Broker Count + ja-JP: フェンスのブローカー数 + - field: GlobalPartitionCount + type: 0 + i18n: + zh-CN: 全局分区数量 + en-US: Global Partition Count + ja-JP: パーティション数 + - field: GlobalTopicCount + type: 0 + i18n: + zh-CN: 全局主题数量 + en-US: Global Topic Count + ja-JP: トピック数 + - field: OfflinePartitionsCount + type: 0 + i18n: + zh-CN: 离线分区数量 + en-US: Offline Partitions Count + ja-JP: オフラインのパーティション数 + - field: PreferredReplicaImbalanceCount + type: 0 + i18n: + zh-CN: 首选副本不平衡数量 + en-US: Preferred Replica Imbalance Count + ja-JP: 優先レプリカ不均衡数 + - field: ReplicasIneligibleToDeleteCount + type: 0 + i18n: + zh-CN: 不能删除的副本数量 + en-US: Replicas Ineligible To Delete Count + ja-JP: 削除できないレプリカ数 + - field: ReplicasToDeleteCount + type: 0 + i18n: + zh-CN: 待删除副本数量 + en-US: Replicas To Delete Count + ja-JP: 削除待ちのレプリカ数 + - field: TopicsIneligibleToDeleteCount + type: 0 + i18n: + zh-CN: 不能删除的主题数量 + en-US: Topics Ineligible To Delete Count + ja-JP: 削除できないトピック数 + - field: TopicsToDeleteCount + type: 0 + i18n: + zh-CN: 待删除主题数量 + en-US: Topics To Delete Count + ja-JP: 削除待ちのトピック数 + - field: ZkMigrationState + type: 0 + i18n: + zh-CN: ZooKeeper迁移状态 + en-US: Zk Migration State + ja-JP: ZooKeeperマイグレーション状態 + aliasFields: + - Value->ActiveBrokerCount + - Value->ActiveControllerCount + - Value->ControllerState + - Value->FencedBrokerCount + - Value->GlobalPartitionCount + - Value->GlobalTopicCount + - Value->OfflinePartitionsCount + - Value->PreferredReplicaImbalanceCount + - Value->ReplicasIneligibleToDeleteCount + - Value->ReplicasToDeleteCount + - Value->TopicsIneligibleToDeleteCount + - Value->TopicsToDeleteCount + - Value->ZkMigrationState + calculates: + - ActiveBrokerCount=Value->ActiveBrokerCount + - ActiveControllerCount=Value->ActiveControllerCount + - ControllerState=Value->ControllerState + - FencedBrokerCount=Value->FencedBrokerCount + - GlobalPartitionCount=Value->GlobalPartitionCount + - GlobalTopicCount=Value->GlobalTopicCount + - OfflinePartitionsCount=Value->OfflinePartitionsCount + - PreferredReplicaImbalanceCount=Value->PreferredReplicaImbalanceCount + - ReplicasIneligibleToDeleteCount=Value->ReplicasIneligibleToDeleteCount + - ReplicasToDeleteCount=Value->ReplicasToDeleteCount + - TopicsIneligibleToDeleteCount=Value->TopicsIneligibleToDeleteCount + - TopicsToDeleteCount=Value->TopicsToDeleteCount + - ZkMigrationState=Value->ZkMigrationState + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: kafka.controller:type=KafkaController,name=* + url: ^_^url^_^ + + + + - name: broker_handler_avg_percent + i18n: + zh-CN: Broker处理器平均百分比 + en-US: Broker Handler Avg Percent + ja-JP: ブローカーハンドラの平均パーセント + priority: 6 + fields: + - field: EventType + type: 1 + i18n: + zh-CN: 事件类型 + en-US: Event Type + ja-JP: イベントタイプ + - field: RateUnit + type: 1 + i18n: + zh-CN: 速率单位 + en-US: Rate Unit + ja-JP: レート単位 + - field: Count + type: 0 + i18n: + zh-CN: 计数 + en-US: Count + ja-JP: カウント + - field: MeanRate + type: 0 + i18n: + zh-CN: 平均速率 + en-US: Mean Rate + ja-JP: 平均レート + - field: OneMinuteRate + type: 0 + i18n: + zh-CN: 一分钟速率 + en-US: One Minute Rate + ja-JP: 1分間のレート + - field: FiveMinuteRate + type: 0 + i18n: + zh-CN: 五分钟速率 + en-US: Five Minute Rate + ja-JP: 5分間のレート + - field: FifteenMinuteRate + type: 0 + i18n: + zh-CN: 十五分钟速率 + en-US: Fifteen Minute Rate + ja-JP: 15分間のレート + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent + url: ^_^url^_^ + + - name: kafka_replica_manager_metrics + i18n: + zh-CN: Kafka副本管理器指标 + en-US: Kafka Replica Manager Metrics + ja-JP: Kafkaレプリカマネジャーのメトリクス + priority: 6 + fields: + - field: AtMinIsrPartitionCount + type: 0 + i18n: + zh-CN: 达到最小ISR的分区数 + en-US: At Min ISR Partition Count + ja-JP: 最小ISRパーティション数 + - field: FailedIsrUpdatesPerSec + type: 0 + i18n: + zh-CN: 每秒失败ISR更新数 + en-US: Failed ISR Updates Per Sec + ja-JP: 1秒あたりのISR更新失敗数 + - field: IsrExpandsPerSec + type: 0 + i18n: + zh-CN: 每秒ISR扩展数 + en-US: ISR Expands Per Sec + ja-JP: 1秒あたりのISR拡張数 + - field: IsrShrinksPerSec + type: 0 + i18n: + zh-CN: 每秒ISR收缩数 + en-US: ISR Shrinks Per Sec + ja-JP: 1秒あたりのISR収縮数 + - field: LeaderCount + type: 0 + i18n: + zh-CN: 领导者数量 + en-US: Leader Count + ja-JP: リーダー数 + - field: OfflineReplicaCount + type: 0 + i18n: + zh-CN: 离线副本数量 + en-US: Offline Replica Count + ja-JP: オフラインのレプリカ数 + - field: PartitionCount + type: 0 + i18n: + zh-CN: 分区总数 + en-US: Partition Count + ja-JP: パーティション数 + - field: PartitionsWithLateTransactionsCount + type: 0 + i18n: + zh-CN: 含有延迟交易的分区数 + en-US: Partitions With Late Transactions Count + ja-JP: 遅いトランザクションのあるパーティション数 + - field: ProducerIdCount + type: 0 + i18n: + zh-CN: 生产者ID数量 + en-US: Producer ID Count + ja-JP: 生産者ID数 + - field: ReassigningPartitions + type: 0 + i18n: + zh-CN: 正在重新分配的分区数 + en-US: Reassigning Partitions + ja-JP: 再割り当てのパーティション数 + - field: UnderMinIsrPartitionCount + type: 0 + i18n: + zh-CN: 低于最小ISR的分区数 + en-US: Under Min ISR Partition Count + ja-JP: 最小ISR未満のパーティション数 + - field: UnderReplicatedPartitions + type: 0 + i18n: + zh-CN: 副本数低于预期的分区数量 + en-US: Under Replicated Partitions + ja-JP: レプリカ未満のパーティション数 + aliasFields: + - Value->LeaderCount + - Value->AtMinIsrPartitionCount + - Value->FailedIsrUpdatesPerSec + - Value->IsrExpandsPerSec + - Value->IsrShrinksPerSec + - Value->OfflineReplicaCount + - Value->PartitionCount + - Value->PartitionsWithLateTransactionsCount + - Value->ProducerIdCount + - Value->ReassigningPartitions + - Value->UnderMinIsrPartitionCount + - Value->UnderReplicatedPartitions + calculates: + - LeaderCount=Value->LeaderCount + - AtMinIsrPartitionCount=Value->AtMinIsrPartitionCount + - FailedIsrUpdatesPerSec=Value->FailedIsrUpdatesPerSec + - IsrExpandsPerSec=Value->IsrExpandsPerSec + - IsrShrinksPerSec=Value->IsrShrinksPerSec + - OfflineReplicaCount=Value->OfflineReplicaCount + - PartitionCount=Value->PartitionCount + - PartitionsWithLateTransactionsCount=Value->PartitionsWithLateTransactionsCount + - ProducerIdCount=Value->ProducerIdCount + - ReassigningPartitions=Value->ReassigningPartitions + - UnderMinIsrPartitionCount=Value->UnderMinIsrPartitionCount + - UnderReplicatedPartitions=Value->UnderReplicatedPartitions + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: kafka.server:type=ReplicaManager,name=* + url: ^_^url^_^ + + + - name: topic_total_bytes_in_persec + i18n: + zh-CN: 每秒主题流入字节 + en-US: Total Bytes In Per Second + ja-JP: 1秒あたりのトピック合計受信されたバイト + priority: 7 + fields: + - field: EventType + type: 1 + i18n: + zh-CN: 事件类型 + en-US: Event Type + ja-JP: イベントタイプ + - field: RateUnit + type: 1 + i18n: + zh-CN: 速率单位 + en-US: Rate Unit + ja-JP: レート単位 + - field: MeanRate + type: 0 + i18n: + zh-CN: 平均速率 + en-US: Mean Rate + ja-JP: 平均レート + - field: OneMinuteRate + type: 0 + i18n: + zh-CN: 一分钟速率 + en-US: One Minute Rate + ja-JP: 1分間のレート + - field: FiveMinuteRate + type: 0 + i18n: + zh-CN: 五分钟速率 + en-US: Five Minute Rate + ja-JP: 5分間のレート + - field: FifteenMinuteRate + type: 0 + i18n: + zh-CN: 十五分钟速率 + en-US: Fifteen Minute Rate + ja-JP: 15分間のレート + - field: Count + type: 0 + i18n: + zh-CN: 计数 + en-US: Count + ja-JP: カウント + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec + url: ^_^url^_^ + + - name: each_topic_bytes_in_persec + i18n: + zh-CN: 各主题每秒流入字节 + en-US: Bytes In Per Topic Per Second + ja-JP: 各トピックの1秒あたりの受信されたバイト + priority: 7 + fields: + - field: topic + type: 1 + i18n: + zh-CN: 主题名称 + en-US: Topic Name + ja-JP: トピック名 + - field: EventType + type: 1 + i18n: + zh-CN: 事件类型 + en-US: Event Type + ja-JP: イベントタイプ + - field: RateUnit + type: 1 + i18n: + zh-CN: 速率单位 + en-US: Rate Unit + ja-JP: レート単位 + - field: MeanRate + type: 0 + i18n: + zh-CN: 平均速率 + en-US: Mean Rate + ja-JP: 平均レート + - field: OneMinuteRate + type: 0 + i18n: + zh-CN: 一分钟速率 + en-US: One Minute Rate + ja-JP: 1分間のレート + - field: FiveMinuteRate + type: 0 + i18n: + zh-CN: 五分钟速率 + en-US: Five Minute Rate + ja-JP: 5分間のレート + - field: FifteenMinuteRate + type: 0 + i18n: + zh-CN: 十五分钟速率 + en-US: Fifteen Minute Rate + ja-JP: 15分間のレート + - field: Count + type: 0 + i18n: + zh-CN: 计数 + en-US: Count + ja-JP: カウント + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec,topic=* + url: ^_^url^_^ + + - name: topic_bytes_out_persec + i18n: + zh-CN: 主题每秒流出字节 + en-US: Total Bytes Out Per Second + ja-JP: 1秒あたりのトピック合計転送されたバイト + priority: 8 + fields: + - field: EventType + type: 1 + i18n: + zh-CN: 事件类型 + en-US: Event Type + ja-JP: イベントタイプ + - field: RateUnit + type: 1 + i18n: + zh-CN: 速率单位 + en-US: Rate Unit + ja-JP: レート単位 + - field: MeanRate + type: 0 + i18n: + zh-CN: 平均速率 + en-US: Mean Rate + ja-JP: 平均レート + - field: OneMinuteRate + type: 0 + i18n: + zh-CN: 一分钟速率 + en-US: One Minute Rate + ja-JP: 1分間のレート + - field: FiveMinuteRate + type: 0 + i18n: + zh-CN: 五分钟速率 + en-US: Five Minute Rate + ja-JP: 5分間のレート + - field: FifteenMinuteRate + type: 0 + i18n: + zh-CN: 十五分钟速率 + en-US: Fifteen Minute Rate + ja-JP: 15分間のレート + - field: Count + type: 0 + i18n: + zh-CN: 计数 + en-US: Count + ja-JP: カウント + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: kafka.server:type=BrokerTopicMetrics,name=BytesOutPerSec + url: ^_^url^_^ + + - name: each_topic_bytes_out_persec + i18n: + zh-CN: 各主题每秒流出字节 + en-US: Bytes Out Per Topic Per Second + ja-JP: 各トピックの1秒あたりの転送されたバイト + priority: 9 + fields: + - field: topic + type: 1 + i18n: + zh-CN: 主题名称 + en-US: Topic Name + ja-JP: トピック名 + - field: EventType + type: 1 + i18n: + zh-CN: 事件类型 + en-US: Event Type + ja-JP: イベントタイプ + - field: RateUnit + type: 1 + i18n: + zh-CN: 速率单位 + en-US: Rate Unit + ja-JP: レート単位 + - field: MeanRate + type: 0 + i18n: + zh-CN: 平均速率 + en-US: Mean Rate + ja-JP: 平均レート + - field: OneMinuteRate + type: 0 + i18n: + zh-CN: 一分钟速率 + en-US: One Minute Rate + ja-JP: 1分間のレート + - field: FiveMinuteRate + type: 0 + i18n: + zh-CN: 五分钟速率 + en-US: Five Minute Rate + ja-JP: 5分間のレート + - field: FifteenMinuteRate + type: 0 + i18n: + zh-CN: 十五分钟速率 + en-US: Fifteen Minute Rate + ja-JP: 15分間のレート + - field: Count + type: 0 + i18n: + zh-CN: 计数 + en-US: Count + ja-JP: カウント + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: kafka.server:type=BrokerTopicMetrics,name=BytesOutPerSec,topic=* + url: ^_^url^_^ + + - name: produce_message_conversions_persec + i18n: + zh-CN: 每秒生产消息转换 + en-US: Produce Message Conversions PerSec + ja-JP: 1秒あたりのメッセージ変換数 + priority: 9 + fields: + - field: EventType + type: 1 + i18n: + zh-CN: 事件类型 + en-US: Event Type + ja-JP: イベントタイプ + - field: RateUnit + type: 1 + i18n: + zh-CN: 速率单位 + en-US: Rate Unit + ja-JP: レート単位 + - field: MeanRate + type: 0 + i18n: + zh-CN: 平均速率 + en-US: Mean Rate + ja-JP: 平均レート + - field: OneMinuteRate + type: 0 + i18n: + zh-CN: 一分钟速率 + en-US: One Minute Rate + ja-JP: 1分間のレート + - field: FiveMinuteRate + type: 0 + i18n: + zh-CN: 五分钟速率 + en-US: Five Minute Rate + ja-JP: 5分間のレート + - field: FifteenMinuteRate + type: 0 + i18n: + zh-CN: 十五分钟速率 + en-US: Fifteen Minute Rate + ja-JP: 15分間のレート + - field: Count + type: 0 + i18n: + zh-CN: 计数 + en-US: Count + ja-JP: カウント + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: kafka.server:type=BrokerTopicMetrics,name=ProduceMessageConversionsPerSec + url: ^_^url^_^ + + - name: produce_total_requests_persec + i18n: + zh-CN: 每秒生产总请求数 + en-US: Produce Total Requests PerSec + ja-JP: 1秒あたりの合計リクエスト数 + priority: 10 + fields: + - field: EventType + type: 1 + i18n: + zh-CN: 事件类型 + en-US: Event Type + ja-JP: イベントタイプ + - field: RateUnit + type: 1 + i18n: + zh-CN: 速率单位 + en-US: Rate Unit + ja-JP: レート単位 + - field: MeanRate + type: 0 + i18n: + zh-CN: 平均速率 + en-US: Mean Rate + ja-JP: 平均レート + - field: OneMinuteRate + type: 0 + i18n: + zh-CN: 一分钟速率 + en-US: One Minute Rate + ja-JP: 1分間のレート + - field: FiveMinuteRate + type: 0 + i18n: + zh-CN: 五分钟速率 + en-US: Five Minute Rate + ja-JP: 5分間のレート + - field: FifteenMinuteRate + type: 0 + i18n: + zh-CN: 十五分钟速率 + en-US: Fifteen Minute Rate + ja-JP: 15分間のレート + - field: Count + type: 0 + i18n: + zh-CN: 计数 + en-US: Count + ja-JP: カウント + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: kafka.server:type=BrokerTopicMetrics,name=TotalProduceRequestsPerSec + url: ^_^url^_^ + + - name: kafka_group_metrics + i18n: + zh-CN: Kafka消费者组指标 + en-US: Kafka Group Metrics + ja-JP: Kafka消費者グループメトリクス + priority: 11 + fields: + - field: NumGroups + type: 0 + i18n: + zh-CN: 群组数量 + en-US: Num Groups + ja-JP: 消費者グループ総数 + - field: NumGroupsCompletingRebalance + type: 0 + i18n: + zh-CN: 正在完成重新平衡的群组数量 + en-US: Num Groups Completing Rebalance + ja-JP: リバランス中の消費者グループ数 + - field: NumGroupsDead + type: 0 + i18n: + zh-CN: 死亡群组数量 + en-US: Num Groups Dead + ja-JP: デッドの消費者グループ数 + - field: NumGroupsEmpty + type: 0 + i18n: + zh-CN: 空群组数量 + en-US: Num Groups Empty + ja-JP: 空の消費者グループ数 + - field: NumGroupsPreparingRebalance + type: 0 + i18n: + zh-CN: 正在准备重新平衡的群组数量 + en-US: Num Groups Preparing Rebalance + ja-JP: リバランス準備中の消費者グループ数 + - field: NumGroupsStable + type: 0 + i18n: + zh-CN: 稳定群组数量 + en-US: Num Groups Stable + ja-JP: 安定した消費者グループ数 + - field: NumOffsets + type: 0 + i18n: + zh-CN: 偏移量数量 + en-US: Num Offsets + ja-JP: オフセット数 + aliasFields: + - Value->NumGroups + - Value->NumGroupsCompletingRebalance + - Value->NumGroupsDead + - Value->NumGroupsEmpty + - Value->NumGroupsPreparingRebalance + - Value->NumGroupsStable + - Value->NumOffsets + calculates: + - NumGroups=Value->NumGroups + - NumGroupsCompletingRebalance=Value->NumGroupsCompletingRebalance + - NumGroupsDead=Value->NumGroupsDead + - NumGroupsEmpty=Value->NumGroupsEmpty + - NumGroupsPreparingRebalance=Value->NumGroupsPreparingRebalance + - NumGroupsStable=Value->NumGroupsStable + - NumOffsets=Value->NumOffsets + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + # kafka.server:type=GroupMetadataManager || kafka.coordinator.group:type=GroupMetadataManager + objectName: kafka.*:type=GroupMetadataManager,name=* + url: ^_^url^_^ + + diff --git a/hertzbeat-manager/src/main/resources/define/app-kafka_client.yml b/hertzbeat-manager/src/main/resources/define/app-kafka_client.yml new file mode 100644 index 0000000..8041ee8 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-kafka_client.yml @@ -0,0 +1,221 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +category: mid +app: kafka_client +name: + zh-CN: Kafka消息系统(客户端) + en-US: Kafka Message(Client) + ja-JP: Kafkaメッセージングシステム(クライアント) + +help: + zh-CN: HertzBeat 使用 <a href="https://hertzbeat.apache.org/zh-cn/docs/help/kafka_client">Kafka Admin Client</a> 对 Kafka 的通用指标进行采集监控。 + en-US: HertzBeat uses <a href='https://hertzbeat.apache.org/docs/help/kafka_client'>Kafka Admin Client</a> to monitoring kafka general metrics. + zh-TW: HertzBeat 使用 <a href="https://hertzbeat.apache.org/zh-cn/docs/help/kafka_client">Kafka Admin Client</a> 對 Kafka 的通用指標進行采集監控。 + ja-JP: HertzBeat は <a href="https://hertzbeat.apache.org/docs/help/kafka_client">Kafka Admin Clientを介して</a> Kafkaの一般的なパフォーマンスのメトリクスを監視します。 + +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/kafka_client + en-US: https://hertzbeat.apache.org/docs/help/kafka_client + +params: + - field: host + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + required: true + defaultValue: 9092 + - field: monitorInternalTopic + name: + zh-CN: 是否监控内部主题 + en-US: Monitor Internal Topic + ja-JP: 内部トピックを監視するかどうか + type: boolean + required: true + defaultValue: false + +metrics: + - name: topic_list + i18n: + zh-CN: 主题列表 + en-US: Topic List + ja-JP: トピック一覧 + priority: 0 + fields: + - field: TopicName + type: 1 + i18n: + zh-CN: 主题名称 + en-US: Topic Name + ja-JP: トピック名 + protocol: kclient + kclient: + host: ^_^host^_^ + port: ^_^port^_^ + monitorInternalTopic: ^_^monitorInternalTopic^_^ + command: topic-list + - name: topic_detail + i18n: + zh-CN: 主题详细信息 + en-US: Topic Detail Info + ja-JP: トピック詳細情報 + priority: 1 + fields: + - field: TopicName + type: 1 + i18n: + zh-CN: 主题名称 + en-US: Topic Name + ja-JP: トピック名 + - field: PartitionNum + type: 1 + i18n: + zh-CN: 分区数量 + en-US: Partition Num + ja-JP: パーティション数 + - field: PartitionLeader + type: 1 + i18n: + zh-CN: 分区领导者 + en-US: Partition Leader + ja-JP: パーティションリーダー + - field: BrokerHost + type: 1 + i18n: + zh-CN: Broker主机 + en-US: Broker Host + ja-JP: ブローカーホスト + - field: BrokerPort + type: 1 + i18n: + zh-CN: Broker端口 + en-US: Broker Port + ja-JP: ブローカーポート + - field: ReplicationFactorSize + type: 1 + i18n: + zh-CN: 复制因子大小 + en-US: Replication Factor Size + ja-JP: レプリカファクターのサイズ + - field: ReplicationFactor + type: 1 + i18n: + zh-CN: 复制因子 + en-US: Replication Factor + ja-JP: レプリカファクター + protocol: kclient + kclient: + host: ^_^host^_^ + port: ^_^port^_^ + monitorInternalTopic: ^_^monitorInternalTopic^_^ + command: topic-describe + - name: topic_offset + i18n: + zh-CN: 主题偏移量 + en-US: Topic Offset + ja-JP: トピックオフセット + priority: 2 + # Kafka offset does not need to be obtained frequently, as getting it too quickly will affect performance + interval: 300 + fields: + - field: TopicName + label: true + type: 1 + i18n: + zh-CN: 主题名称 + en-US: Topic Name + ja-JP: トピック名 + - field: PartitionNum + label: true + type: 1 + i18n: + zh-CN: 分区号 + en-US: Partition Num + ja-JP: パーティション数 + - field: earliest + type: 0 + i18n: + zh-CN: 最早偏移量 + en-US: Earliest Offset + ja-JP: 最早オフセット + - field: latest + type: 0 + i18n: + zh-CN: 最新偏移量 + en-US: Latest Offset + ja-JP: 最新オフセット + protocol: kclient + kclient: + host: ^_^host^_^ + port: ^_^port^_^ + monitorInternalTopic: ^_^monitorInternalTopic^_^ + command: topic-offset + - name: consumer_detail + i18n: + zh-CN: 消费者组情况 + en-US: Consumer Detail Info + ja-JP: 消費者グループ詳細情報 + priority: 3 + # Kafka offset does not need to be obtained frequently, as getting it too quickly will affect performance + interval: 300 + fields: + - field: GroupId + label: true + type: 1 + i18n: + zh-CN: 消费者组ID + en-US: Consumer Group ID + ja-JP: 消費者グループID + - field: group_member_num + type: 1 + i18n: + zh-CN: 消费者实例数量 + en-US: Group Member Num + ja-JP: 消費者グループのメンバー数 + - field: Topic + label: true + type: 1 + i18n: + zh-CN: 订阅主题名称 + en-US: Subscribed Topic Name + ja-JP: 購読されたトピック名 + - field: offset_of_each_partition + type: 1 + i18n: + zh-CN: 各分区偏移量 + en-US: Offset of Each Partition + ja-JP: 各パーティションのオフセット + - field: Lag + type: 0 + i18n: + zh-CN: 落后偏移量 + en-US: Total Lag + ja-JP: ラグオフセット + protocol: kclient + kclient: + host: ^_^host^_^ + port: ^_^port^_^ + monitorInternalTopic: ^_^monitorInternalTopic^_^ + command: consumer-detail \ No newline at end of file diff --git a/hertzbeat-manager/src/main/resources/define/app-kafka_promql.yml b/hertzbeat-manager/src/main/resources/define/app-kafka_promql.yml new file mode 100644 index 0000000..01f09b9 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-kafka_promql.yml @@ -0,0 +1,374 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: custom +# The monitoring type eg: linux windows tomcat mysql aws... +app: kafka_promql +name: + zh-CN: Kafka-PromQL + en-US: Kafka-PromQL + ja-JP: Kafka-PromQL +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 Prometheus PromQL 从 Prometheus 服务器中查询到 Kafka 的通用指标数据来进行监控。此方案适用于 Prometheus 已监控 Kafka,需要从 Prometheus 服务器抓取 Kafka 的监控数据。<br>您可以点击 “<i>新建 Kafka-PromQL</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses Prometheus PromQL query metrics data from Prometheus Server to monitoring Kafka. This solution is suitable for Prometheus to monitor Kafka, and it need to capture Kafka monitoring data from the Prometheus server. <br>You could click the "<i>New Kafka-PromQL</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 Prometheus PromQL 從 Prometheus 服務器中查詢到 Kafka 的通用指標數據來進行監控。此方案適用于 Prometheus 已監控 Kafka,需要從 Prometheus 服務器抓取 Kafka 的監控數據。<br>您可以點擊 “<i>新建 Kafka-PromQL</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は Prometheus PromQL を介して Prometheus サーバーに Kafka の一般的なパフォーマンスのメトリクスをクエリして監視します。このシナリオは、PrometheusがすでにKafkaを監視しており、PrometheusサーバーからKafkaの監視データを取得する必要がある場合に適用されます。<br>「<i>新規 Kafka-PromQL</i>」をクリックして設定しましょう。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/kafka_promql + en-US: https://hertzbeat.apache.org/docs/help/kafka_promql +params: + - field: host + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + required: true + defaultValue: 9090 + - field: method + name: + zh-CN: 请求方式 + en-US: Method + ja-JP: リクエストメソッド + type: radio + required: true + options: + - label: GET请求 + value: GET + - label: POST请求 + value: POST + - label: PUT请求 + value: PUT + - label: DELETE请求 + value: DELETE + defaultValue: GET + - field: uri + name: + zh-CN: 相对路径 + en-US: URI + ja-JP: URI + type: text + limit: 200 + required: true + placeholder: 'API地址除IP端口外的路径 例如:/v2/book/bar' + defaultValue: /api/v1/query + - field: ssl + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL + type: boolean + required: false + - field: headers + name: + zh-CN: 请求Headers + en-US: Headers + ja-JP: ヘッダ + type: key-value + required: false + keyAlias: Header Name + valueAlias: Header Value + - field: params + name: + zh-CN: 查询Params + en-US: Params + ja-JP: パラメータ + type: key-value + required: false + keyAlias: Param Key + valueAlias: Param Value + - field: contentType + name: + zh-CN: Content-Type + en-US: Content-Type + ja-JP: コンテンツタイプ + type: text + placeholder: '请求BODY资源类型' + required: false + hide: true + - field: payload + name: + zh-CN: 请求BODY + en-US: BODY + ja-JP: ボディ + type: textarea + placeholder: 'POST PUT请求时有效' + required: false + hide: true + - field: authType + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + type: radio + required: false + hide: true + options: + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + hide: true + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + hide: true +metrics: + # metrics - kafka_brokers + - name: kafka_brokers + i18n: + zh-CN: Kafka Broker 数量 + en-US: Kafka Broker Count + ja-JP: Kafkaブローカー数量 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, i18n-metric name i18n label, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: __name__ + type: 1 + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: metric_instance + type: 1 + i18n: + zh-CN: 实例 + en-US: Instance + ja-JP: インスタンス + - field: timestamp + type: 1 + i18n: + zh-CN: 时间戳 + en-US: Timestamp + ja-JP: タイムスタンプ + - field: value + type: 1 + i18n: + zh-CN: 数值 + en-US: Value + ja-JP: 値 + aliasFields: + - __name__ + - instance + - timestamp + - value + calculates: + - metric_instance=instance + # The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # The config content when protocol is http + http: + # Host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: ^_^uri^_^ + # request method: GET POST PUT DELETE PATCH + method: ^_^method^_^ + # whether to enable ssl/tls, that is, http or https, the default is false + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + # request header content + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + # request parameter content + params: + query: kafka_brokers + # Authentication method: Basic Auth, Digest Auth, Bearer Token + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # Response data parsing methods: default-system rule,jsonPath-jsonPath script,website-api availability indicator monitoring + parseType: PromQL + + - name: kafka_topic_partitions + i18n: + zh-CN: Kafka Topic 分区数量 + en-US: Kafka Topic Partitions + ja-JP: Kafkaトピックのパーティション数量 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: __name__ + type: 1 + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: topic + type: 1 + i18n: + zh-CN: 主题 + en-US: Topic + ja-JP: トピック + - field: timestamp + type: 1 + i18n: + zh-CN: 时间戳 + en-US: Timestamp + ja-JP: タイムスタンプ + - field: value + type: 1 + i18n: + zh-CN: 数值 + en-US: Value + ja-JP: 値 + # The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # The config content when protocol is http + http: + # Host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: ^_^uri^_^ + # request method: GET POST PUT DELETE PATCH + method: ^_^method^_^ + # whether to enable ssl/tls, that is, http or https, the default is false + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + # request header content + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + # request parameter content + params: + query: kafka_topic_partitions + # Authentication method: Basic Auth, Digest Auth, Bearer Token + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # Response data parsing method: default - system rules, jsonPath - jsonPath script, website - API availability monitoring. + # todo xmlPath-xmlPath script, PromQL-PromQL data rule + parseType: PromQL + + - name: kafka_server_brokertopicmetrics_bytesinpersec # Query rate per second + i18n: + zh-CN: Kafka Server Broker Topic 每秒字节入 + en-US: Kafka Server Broker Topic Bytes In Per Second + ja-JP: Kafkaサーバーブローカーの1秒あたりのトピック合計受信されたバイト + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: metric_instance + type: 1 + i18n: + zh-CN: 实例 + en-US: Instance + ja-JP: インスタンス + - field: job + type: 1 + i18n: + zh-CN: 任务 + en-US: Job + ja-JP: タスク + - field: topic + type: 1 + i18n: + zh-CN: 主题 + en-US: Topic + ja-JP: トピック + - field: timestamp + type: 1 + i18n: + zh-CN: 时间戳 + en-US: Timestamp + ja-JP: タイムスタンプ + - field: value + type: 1 + i18n: + zh-CN: 数值 + en-US: Value + ja-JP: 値 + aliasFields: + - instance + - job + - topic + - timestamp + - value + calculates: + - metric_instance=instance + # The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # The specific collection configuration when the protocol is http + http: + # Host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: ^_^uri^_^ + # request methods:GET POST PUT DELETE PATCH + method: ^_^method^_^ + # whether to enable ssl/tls, that is, http or https, the default is false + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + # Request header content + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + # Request parameter content + params: + query: kafka_server_brokertopicmetrics_bytesinpersec + # Authentication method: Basic Auth, Digest Auth, Bearer Token + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # Response data parsing method: default - system rules, jsonPath - jsonPath script, website - API availability monitoring. + # todo xmlPath-xmlPath script, PromQL-PromQL data rule + parseType: PromQL diff --git a/hertzbeat-manager/src/main/resources/define/app-kingbase.yml b/hertzbeat-manager/src/main/resources/define/app-kingbase.yml new file mode 100644 index 0000000..08c8272 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-kingbase.yml @@ -0,0 +1,837 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: kingbase +# The monitoring i18n name +name: + zh-CN: Kingbase数据库 + en-US: Kingbase DB + ja-JP: Kingbaseデータベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 Kingbase 数据库的通用性能指标 (basic、state、activity etc) 进行采集监控,支持版本为 KingbaseV8r6+。<br>您可以点击“<i>新建 Kingbase 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC Protocol</a> to configure SQL for collecting general metrics of Kingbase database (basic、state、activity etc). Supported version is KingbaseV8r6+. <br>You can click "<i>New Kingbase Database</i>" and configure it, or select "<i>More Action</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 協議</a> 通過配置 SQL 對 Kingbase 數據庫的通用性能指標 (basic、state、activity etc)進行采集監控,支持版本爲 KingbaseV8r6+。<br>您可以點擊“<i>新建 Kingbase 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBCプロトコルを介して</a> Kingbase データベース(V8r6+)の一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Kingbase データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/kingbase + en-US: https://hertzbeat.apache.org/docs/help/kingbase +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 5432 + - field: timeout + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + type: number + range: '[400,200000]' + required: false + hide: true + defaultValue: 6000 + - field: database + name: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + type: text + defaultValue: kingbase + required: false + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + - field: url + name: + zh-CN: URL + en-US: URL + ja-JP: URL + type: text + required: false + hide: true + +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 基本信息 + en-US: Basic Info + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: server_version + type: 1 + label: true + i18n: + zh-CN: 服务器版本 + en-US: Server Version + ja-JP: バージョン + - field: port + type: 1 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: server_encoding + type: 1 + i18n: + zh-CN: 服务器编码 + en-US: Server Encoding + ja-JP: サーバーのエンコード + - field: data_directory + type: 1 + i18n: + zh-CN: 数据目录 + en-US: Data Directory + ja-JP: データディレクトリ + - field: max_connections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # host: ipv4 ipv6 host + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + # database platform name + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + # sql + sql: select name, setting as value from pg_settings where name = 'max_connections' or name = 'server_version' or name = 'server_encoding' or name = 'port' or name = 'data_directory'; + # JDBC url + url: ^_^url^_^ + + - name: state + i18n: + zh-CN: 状态信息 + en-US: State Info + ja-JP: 状態情報 + priority: 1 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: conflicts + type: 0 + unit: times + i18n: + zh-CN: 冲突次数 + en-US: Conflicts + ja-JP: コンフリクト回数 + - field: deadlocks + type: 0 + unit: times + i18n: + zh-CN: 死锁次数 + en-US: Deadlocks + ja-JP: デッドロック回数 + - field: blks_read + type: 0 + unit: blocks per second + i18n: + zh-CN: 读取块 + en-US: Blocks Read + ja-JP: 読み取られたブロック + - field: blks_hit + type: 0 + unit: blocks per second + i18n: + zh-CN: 命中块 + en-US: Blocks Hit + ja-JP: ヒットブロック + - field: blk_read_time + type: 0 + unit: ms + i18n: + zh-CN: 读取时间 + en-US: Read Time + ja-JP: 読み取られタイム + - field: blk_write_time + type: 0 + unit: ms + i18n: + zh-CN: 写入时间 + en-US: Write Time + ja-JP: 書き込まれ時間 + - field: stats_reset + type: 1 + i18n: + zh-CN: 统计重置 + en-US: Stats Reset + ja-JP: 統計リセット + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: SELECT COALESCE(datname,'shared-object') as db_name, conflicts, deadlocks, blks_read, blks_hit, blk_read_time, blk_write_time, stats_reset from pg_stat_database where (datname != 'template1' and datname != 'template0') or datname is null; + url: ^_^url^_^ + + - name: activity + i18n: + zh-CN: 活动信息 + en-US: Activity Info + ja-JP: 活動情報 + priority: 2 + fields: + - field: running + type: 0 + unit: sbc + i18n: + zh-CN: 运行中 + en-US: Running + ja-JP: 実行中 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: SELECT count(*) as running FROM pg_stat_activity WHERE NOT pid=pg_backend_pid(); + url: ^_^url^_^ + + - name: resource_config + i18n: + zh-CN: 资源配置 + en-US: Resource Config + ja-JP: リソース設定 + priority: 3 + fields: + - field: work_mem + type: 0 + unit: MB + i18n: + zh-CN: 工作内存 + en-US: Work Memory + ja-JP: ワークメモリ + - field: shared_buffers + type: 0 + unit: MB + i18n: + zh-CN: 共享缓冲区 + en-US: Shared Buffers + ja-JP: 共有バッファ + - field: autovacuum + type: 1 + i18n: + zh-CN: 自动清理 + en-US: Auto Vacuum + ja-JP: オートバキューム + - field: max_connections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + - field: effective_cache_size + type: 0 + unit: MB + i18n: + zh-CN: 有效缓存大小 + en-US: Effective Cache Size + ja-JP: キャッシュサイズ + - field: wal_buffers + type: 0 + unit: MB + i18n: + zh-CN: WAL缓冲区 + en-US: WAL Buffers + ja-JP: WALバッファ + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: columns + sql: show all; + url: ^_^url^_^ + + - name: connection + i18n: + zh-CN: 连接信息 + en-US: Connection Info + ja-JP: 接続情報 + priority: 4 + fields: + - field: active + type: 0 + i18n: + zh-CN: 活动连接 + en-US: Active Connection + ja-JP: 活躍的な接続 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: select count(1) as active from pg_stat_activity; + url: ^_^url^_^ + + - name: connection_state + i18n: + zh-CN: 连接状态 + en-US: Connection State + ja-JP: 接続状態 + priority: 5 + fields: + - field: state + type: 1 + label: true + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + - field: num + type: 0 + i18n: + zh-CN: 数量 + en-US: Num + ja-JP: 数量 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select COALESCE(state, 'other') as state, count(*) as num from pg_stat_activity group by state; + url: ^_^url^_^ + + - name: connection_db + i18n: + zh-CN: 连接数据库 + en-US: Connection Db + ja-JP: 接続データベース + priority: 6 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: active + type: 0 + i18n: + zh-CN: 活动连接 + en-US: Active Connection + ja-JP: 活躍的な接続 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select count(*) as active, COALESCE(datname, 'other') as db_name from pg_stat_activity group by datname; + url: ^_^url^_^ + + - name: tuple + i18n: + zh-CN: 元组信息 + en-US: Tuple Info + ja-JP: 組情報 + priority: 7 + fields: + - field: fetched + type: 0 + i18n: + zh-CN: 获取次数 + en-US: Fetched + ja-JP: フェッチ回数 + - field: returned + type: 0 + i18n: + zh-CN: 返回次数 + en-US: Returned + ja-JP: 戻る回数 + - field: inserted + type: 0 + i18n: + zh-CN: 插入次数 + en-US: Inserted + ja-JP: インサート回数 + - field: updated + type: 0 + i18n: + zh-CN: 更新次数 + en-US: Updated + ja-JP: 更新回数 + - field: deleted + type: 0 + i18n: + zh-CN: 删除次数 + en-US: Deleted + ja-JP: 削除回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select sum(tup_fetched) as fetched, sum(tup_updated) as updated, sum(tup_deleted) as deleted, sum(tup_inserted) as inserted, sum(tup_returned) as returned from pg_stat_database; + url: ^_^url^_^ + + - name: temp_file + i18n: + zh-CN: 临时文件 + en-US: Temp File + ja-JP: 一時ファイル + priority: 8 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: num + type: 0 + i18n: + zh-CN: 次数 + en-US: Num + ja-JP: 数量 + - field: Size + type: 0 + unit: B + i18n: + zh-CN: 大小 + en-US: Size + ja-JP: サイズ + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select COALESCE(datname, 'other') as db_name, sum(temp_files) as num, sum(temp_bytes) as size from pg_stat_database group by datname; + url: ^_^url^_^ + + - name: lock + i18n: + zh-CN: 锁信息 + en-US: Lock Info + ja-JP: ロック情報 + priority: 9 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: conflicts + type: 0 + unit: times + i18n: + zh-CN: 冲突次数 + en-US: Conflicts + ja-JP: コンフリクト回数 + - field: deadlocks + type: 0 + unit: times + i18n: + zh-CN: 死锁次数 + en-US: Deadlocks + ja-JP: デッドロック回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: SELECT COALESCE(datname,'shared-object') as db_name, conflicts, deadlocks from pg_stat_database where (datname != 'template1' and datname != 'template0') or datname is null; + url: ^_^url^_^ + + - name: slow_sql + i18n: + zh-CN: 慢查询 + en-US: Slow Sql + ja-JP: スローSQL + priority: 10 + fields: + - field: sql_text + type: 1 + label: true + i18n: + zh-CN: SQL语句 + en-US: SQL Text + ja-JP: SQL文のテキスト + - field: calls + type: 0 + i18n: + zh-CN: 调用次数 + en-US: Calls + ja-JP: コール回数 + - field: rows + type: 0 + i18n: + zh-CN: 行数 + en-US: Rows + ja-JP: 行 + - field: avg_time + type: 0 + unit: ms + i18n: + zh-CN: 平均时间 + en-US: Avg Time + ja-JP: 平均時間 + - field: total_time + type: 0 + unit: ms + i18n: + zh-CN: 总时间 + en-US: Total Time + ja-JP: 合計時間 + aliasFields: + - query + - calls + - rows + - total_exec_time + - mean_exec_time + calculates: + - sql_text=query + - avg_time=mean_exec_time + - total_time=total_exec_time + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select * from pg_stat_statements; + url: ^_^url^_^ + + - name: transaction + i18n: + zh-CN: 事务信息 + en-US: Transaction Info + ja-JP: トランザクション情報 + priority: 12 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: commits + type: 0 + unit: times + i18n: + zh-CN: 提交次数 + en-US: Commits + ja-JP: コミット回数 + - field: rollbacks + type: 0 + unit: times + i18n: + zh-CN: 回滚次数 + en-US: Rollbacks + ja-JP: ロールバック回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select COALESCE(datname, 'other') as db_name, sum(xact_commit) as commits, sum(xact_rollback) as rollbacks from pg_stat_database group by datname; + url: ^_^url^_^ + + - name: conflicts + i18n: + zh-CN: 冲突信息 + en-US: Conflicts Info + ja-JP: コンフリクト情報 + priority: 13 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: tablespace + type: 0 + i18n: + zh-CN: 表空间 + en-US: Tablespace + ja-JP: 表領域 + - field: lock + type: 0 + i18n: + zh-CN: 锁 + en-US: Lock + ja-JP: ロック + - field: snapshot + type: 0 + i18n: + zh-CN: 快照 + en-US: Snapshot + ja-JP: スナップショット + - field: bufferpin + type: 0 + i18n: + zh-CN: 缓冲区 + en-US: Bufferpin + ja-JP: バッファ + - field: deadlock + type: 0 + i18n: + zh-CN: 死锁 + en-US: Deadlock + ja-JP: デッドロック + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select datname as db_name, confl_tablespace as tablespace, confl_lock as lock, confl_snapshot as snapshot, confl_bufferpin as bufferpin, confl_deadlock as deadlock from pg_stat_database_conflicts; + url: ^_^url^_^ + + - name: cache_hit_ratio + i18n: + zh-CN: 缓存命中率 + en-US: Cache Hit Ratio + ja-JP: キャッシュ命中率 + priority: 14 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: ratio + type: 0 + unit: '%' + i18n: + zh-CN: 命中率 + en-US: Hit Ratio + ja-JP: 命中率 + aliasFields: + - blks_hit + - blks_read + - db_name + calculates: + - ratio=(blks_hit + 1) / (blks_read + blks_hit + 1) * 100 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select datname as db_name, blks_hit, blks_read from pg_stat_database; + url: ^_^url^_^ + + - name: checkpoint + i18n: + zh-CN: Checkpoint信息 + en-US: Checkpoint Info + ja-JP: チェックポイント情報 + priority: 15 + fields: + - field: checkpoint_sync_time + type: 0 + unit: ms + i18n: + zh-CN: Checkpoint同步时间 + en-US: Checkpoint Sync Time + ja-JP: チェックポイント同期時間 + - field: checkpoint_write_time + type: 0 + unit: ms + i18n: + zh-CN: Checkpoint写入时间 + en-US: Checkpoint Write Time + ja-JP: Checkpoint書き込まれた時間 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: select checkpoint_sync_time, checkpoint_write_time from pg_stat_bgwriter; + url: ^_^url^_^ + + - name: buffer + i18n: + zh-CN: Buffer信息 + en-US: Buffer Info + ja-JP: バッファ情報 + priority: 16 + fields: + - field: allocated + type: 0 + i18n: + zh-CN: 已分配 + en-US: Allocated + ja-JP: 割り当てバッファ + - field: fsync_calls_by_backend + type: 0 + i18n: + zh-CN: 后端进程直接执行的文件同步调用次数 + en-US: Fsync Calls By Backend + ja-JP: バックエンド同期コール回数 + - field: written_directly_by_backend + type: 0 + i18n: + zh-CN: 后台写入到数据文件 + en-US: Written Directly By Backend + ja-JP: バックエンドによる直接書き込まれたファイル + - field: written_by_background_writer + type: 0 + i18n: + zh-CN: 后台写入 + en-US: Written By Background Writer + ja-JP: バックグラウンドライターに書き込まれた + - field: written_during_checkpoints + type: 0 + i18n: + zh-CN: 检查点期间写入 + en-US: Written During Checkpoints + ja-JP: チェックポイント中の書き込み + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: select buffers_alloc as allocated, buffers_backend_fsync as fsync_calls_by_backend, buffers_backend as written_directly_by_backend, buffers_clean as written_by_background_writer, buffers_checkpoint as written_during_checkpoints from pg_stat_bgwriter; + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-kubernetes.yml b/hertzbeat-manager/src/main/resources/define/app-kubernetes.yml new file mode 100644 index 0000000..335488a --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-kubernetes.yml @@ -0,0 +1,399 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: cn +# The monitoring type eg: linux windows tomcat mysql aws... +app: kubernetes +# The monitoring i18n name +name: + zh-CN: Kubernetes + en-US: Kubernetes + ja-JP: Kubernetes +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 通过查询 Kubernetes ApiServer api 来对 kubernetes 的通用性能指标(nodes、namespaces、pods、services)进行采集监控。<br><span class='help_module_span'>注意⚠️:为了监控 Kubernetes 中的信息,则需要获取到可访问 Api Server 的授权 TOKEN,让采集请求获取到对应的信息,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/kubernetes'>点击查看获取步骤</a>。</span> + en-US: HertzBeat monitoring Kubernetes general metrics such as nodes, namespaces and pods through querying data from Kubernetes ApiServer api. <br><span class='help_module_span'>Note⚠️:In order to monitor the information of Kubernetes, Hertzbeat need to obtain the authorized TOKEN that can access Api Server. <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/kubernetes'>Click here to view the specific steps.</a></span> + zh-TW: HertzBeat 通過查詢 Kubernetes ApiServer api 來對 kubernetes 的通用性能指標(nodes、namespaces、pods、services)進行采集監控。<br><span class='help_module_span'>注意⚠️:爲了監控 Kubernetes 中的信息,則需要獲取到可訪問 Api Server 的授權 TOKEN,讓采集請求獲取到對應的信息,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/kubernetes'>點擊查看獲取步驟</a>。</span> + ja-JP: HertzBeat は Kubernetes ApiServer api を呼び出し、kubernetes の一般的なパフォーマンスのメトリクスを収集して監視します。<br><span class='help_module_span'>注意⚠️:Kubernetesでメトリクスを監視するためには、Api Serverにアクセスするための認可されたTOKENを取得する必要があります。<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/kubernetes'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/kubernetes + en-US: https://hertzbeat.apache.org/docs/help/kubernetes +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: ApiServer端口 + en-US: ApiServer Port + ja-JP: ApiServerポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 6443 + # field-param field key + - field: authType + # name-param field display i18n name + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: Bearer Token + value: Bearer Token + defaultValue: Bearer Token + - field: token + name: + zh-CN: 认证Token + en-US: Access Token + ja-JP: アクセストークン + type: text + required: true +# collect metrics config list +metrics: + # metrics - nodes + - name: nodes + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: node_name + type: 1 + i18n: + zh-CN: 节点名称 + en-US: Node Name + ja-JP: ノード名 + - field: is_ready + type: 1 + i18n: + zh-CN: 节点就绪状态 + en-US: Node Ready Status + ja-JP: ノード準備完了 + - field: capacity_cpu + type: 0 + i18n: + zh-CN: CPU 容量 + en-US: CPU Capacity + ja-JP: CPU 容量 + - field: allocatable_cpu + type: 0 + i18n: + zh-CN: 可分配 CPU + en-US: Allocatable CPU + ja-JP: 割り当て可能CPU + - field: capacity_memory + type: 0 + unit: Mi + i18n: + zh-CN: 内存容量 + en-US: Memory Capacity + ja-JP: メモリ容量 + - field: allocatable_memory + type: 0 + unit: Mi + i18n: + zh-CN: 可分配内存 + en-US: Allocatable Memory + ja-JP: 割り当て可能CPUメモリ + - field: creation_time + type: 1 + i18n: + zh-CN: 创建时间 + en-US: Creation Time + ja-JP: 作成時間 + - field: uid + type: 1 + i18n: + zh-CN: Uid + en-US: Uid + ja-JP: Uid + label: true + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.metadata.name + - $.status.conditions[?(@.type=='Ready')].status + - $.status.capacity.cpu + - $.status.capacity.memory + - $.status.allocatable.cpu + - $.status.allocatable.memory + - $.metadata.creationTimestamp + - $.metadata.uid + # (optional)mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - node_name=$.metadata.name + - is_ready=$.status.conditions[?(@.type=='Ready')].status + - capacity_cpu=$.status.capacity.cpu + - allocatable_cpu=$.status.allocatable.cpu + - capacity_memory=$.status.capacity.memory + - allocatable_memory=$.status.allocatable.memory + - creation_time=$.metadata.creationTimestamp + - uid=$.metadata.uid + # (optional)field unit mapping and conversion expressions, origin unit -> final unit + units: + - capacity_memory=Ki->Mi + - allocatable_memory=Ki->Mi + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /api/v1/nodes + method: GET + ssl: true + authorization: + type: ^_^authType^_^ + bearerTokenToken: ^_^token^_^ + parseType: jsonPath + parseScript: '$.items.*' + + - name: namespaces + priority: 1 + fields: + - field: namespace + type: 1 + i18n: + zh-CN: 命名空间 + en-US: Namespace + ja-JP: 名前空間 + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + - field: creation_time + type: 1 + i18n: + zh-CN: 创建时间 + en-US: Creation Time + ja-JP: 作成時間 + - field: uid + type: 1 + i18n: + zh-CN: Uid + en-US: Uid + ja-JP: Uid + label: true + aliasFields: + - $.metadata.name + - $.status.phase + - $.metadata.creationTimestamp + - $.metadata.uid + calculates: + - namespace=$.metadata.name + - status=$.status.phase + - creation_time=$.metadata.creationTimestamp + - uid=$.metadata.uid + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /api/v1/namespaces + method: GET + ssl: true + authorization: + type: ^_^authType^_^ + bearerTokenToken: ^_^token^_^ + parseType: jsonPath + parseScript: '$.items.*' + + - name: pods + priority: 2 + fields: + - field: pod + type: 1 + i18n: + zh-CN: Pod名称 + en-US: Pod Name + ja-JP: ポッド名 + - field: namespace + type: 1 + i18n: + zh-CN: 命名空间 + en-US: Namespace + ja-JP: 名前空間 + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + - field: restart + type: 1 + i18n: + zh-CN: 重启次数 + en-US: Restart Count + ja-JP: リスタート回数 + - field: host_ip + type: 1 + i18n: + zh-CN: 主机IP + en-US: Host IP + ja-JP: ホストIP + - field: pod_ip + type: 1 + i18n: + zh-CN: Pod IP + en-US: Pod IP + ja-JP: ポッドIP + - field: creation_time + type: 1 + i18n: + zh-CN: 创建时间 + en-US: Creation Time + ja-JP: 作成時間 + - field: start_time + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Start Time + ja-JP: 起動時間 + - field: uid + type: 1 + i18n: + zh-CN: Uid + en-US: Uid + ja-JP: Uid + label: true + aliasFields: + - $.metadata.name + - $.metadata.namespace + - $.status.phase + - $.spec.restartPolicy + - $.status.hostIP + - $.status.podIP + - $.metadata.creationTimestamp + - $.status.startTime + - $.metadata.uid + calculates: + - pod=$.metadata.name + - namespace=$.metadata.namespace + - status=$.status.phase + - restart=$.spec.restartPolicy + - host_ip=$.status.hostIP + - pod_ip=$.status.podIP + - creation_time=$.metadata.creationTimestamp + - start_time=$.status.startTime + - uid=$.metadata.uid + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /api/v1/pods + method: GET + ssl: true + authorization: + type: ^_^authType^_^ + bearerTokenToken: ^_^token^_^ + parseType: jsonPath + parseScript: '$.items.*' + + - name: services + priority: 3 + fields: + - field: service + type: 1 + i18n: + zh-CN: 服务 + en-US: Service + ja-JP: サービス + - field: namespace + type: 1 + i18n: + zh-CN: 命名空间 + en-US: Namespace + ja-JP: 名前空間 + - field: type + type: 1 + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: タイプ + - field: cluster_ip + type: 1 + i18n: + zh-CN: 集群IP + en-US: Cluster IP + ja-JP: クラスタIP + - field: selector + type: 1 + i18n: + zh-CN: 选择器 + en-US: Selector + ja-JP: セレクター + - field: creation_time + type: 1 + i18n: + zh-CN: 创建时间 + en-US: Creation Time + ja-JP: 作成時間 + - field: uid + type: 1 + i18n: + zh-CN: Uid + en-US: Uid + ja-JP: Uid + label: true + aliasFields: + - $.metadata.name + - $.metadata.namespace + - $.spec.type + - $.spec.clusterIP + - $.spec.selector + - $.metadata.creationTimestamp + - $.metadata.uid + calculates: + - service=$.metadata.name + - namespace=$.metadata.namespace + - type=$.spec.type + - cluster_ip=$.spec.clusterIP + - selector=$.spec.selector + - creation_time=$.metadata.creationTimestamp + - uid=$.metadata.uid + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /api/v1/services + method: GET + ssl: true + authorization: + type: ^_^authType^_^ + bearerTokenToken: ^_^token^_^ + parseType: jsonPath + parseScript: '$.items.*' diff --git a/hertzbeat-manager/src/main/resources/define/app-kvrocks.yml b/hertzbeat-manager/src/main/resources/define/app-kvrocks.yml new file mode 100644 index 0000000..80bc450 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-kvrocks.yml @@ -0,0 +1,807 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: cache +# The monitoring type eg: linux windows tomcat mysql aws... +app: kvrocks +# The monitoring i18n name +name: + zh-CN: Kvrocks 数据库 + en-US: Kvrocks + ja-JP: Kvrocksデータベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 Apache Kvrocks 数据库的通用性能指标进行采集监控(server、clients、memory、persistence、stats、replication、cpu、cluster、commandstats),支持版本为 Apache Kvrocks 2.9.0+。<br>您可以点击“<i>新建 Kvrocks 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat monitors Apache Kvrocks database of general performance metrics such as memory, persistence, replication and so on. The versions we support is Apache Kvrocks 2.9.0+. <br>You could click the "<i>New Kvrocks</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: HertzBeat 對 Apache Kvrocks 數據庫的通用性能指標進行采集監控(server、clients、memory、persistence、stats、replication、cpu、cluster、commandstats),支持版本爲 Apache Kvrocks 2.9.0+。<br>您可以點擊“<i>新建 Kvrocks 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は Apache Kvrocks データベース(2.9.0+)の一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Kvrocks データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/kvrocks + en-US: https://hertzbeat.apache.org/docs/help/kvrocks +# Input params define for monitoring(render web ui by +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 6666 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间 + en-US: Timeout + ja-JP: タイムアウト + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value + defaultValue: 3000 + # field-param field key + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + # field-param field key + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + +# collect metrics config list +metrics: + # metrics - server + - name: server + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + i18n: + zh-CN: 服务器信息 + en-US: Server + ja-JP: サーバー情報 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: kvrocks_version + type: 1 + i18n: + zh-CN: Kvrocks 服务版本 + en-US: Kvrocks Version + ja-JP: Kvrocks バージョン + - field: redis_version + type: 1 + i18n: + zh-CN: Redis 服务版本 + en-US: Redis Version + ja-JP: Redis バージョン + - field: git_sha1 + type: 0 + i18n: + zh-CN: Kvrocks Git SHA1 + en-US: Kvrocks Git SHA1 + ja-JP: Kvrocks Git SHA1 + - field: kvrocks_mode + type: 1 + i18n: + zh-CN: 运行模式 + en-US: Server Mode + ja-JP: サーバーモード + - field: os + type: 1 + i18n: + zh-CN: 操作系统 + en-US: Operating System + ja-JP: オーエス + - field: arch_bits + type: 0 + i18n: + zh-CN: 架构 + en-US: Architecture Bits + ja-JP: アーキテクチャ + - field: multiplexing_api + type: 1 + i18n: + zh-CN: IO多路复用器API + en-US: Multiplexing API + ja-JP: IO多重化API + - field: atomicvar_api + type: 1 + i18n: + zh-CN: 原子操作处理API + en-US: Atomicvar API + ja-JP: 原子操作API + - field: gcc_version + type: 1 + i18n: + zh-CN: GCC版本 + en-US: GCC Version + ja-JP: GCC バージョン + - field: process_id + type: 0 + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: tcp_port + type: 0 + i18n: + zh-CN: TCP/IP监听端口 + en-US: TCP Port + ja-JP: TCP ポート + - field: server_time_usec + type: 0 + i18n: + zh-CN: 服务器时间戳 + en-US: Server Time Usec + ja-JP: サーバー時間 + - field: uptime_in_seconds + type: 0 + i18n: + zh-CN: 运行时长(秒) + en-US: Uptime(Seconds) + ja-JP: アップタイム(秒) + - field: uptime_in_days + type: 0 + i18n: + zh-CN: 运行时长(天) + en-US: Uptime(Days) + ja-JP: アップタイム(日) + - field: hz + type: 0 + i18n: + zh-CN: 事件循环频率 + en-US: hz + ja-JP: hz + - field: configured_hz + type: 0 + i18n: + zh-CN: 配置的事件循环频率 + en-US: Configured hz + ja-JP: Configured hz + - field: lru_clock + type: 0 + i18n: + zh-CN: LRU时钟 + en-US: LRU Clock + ja-JP: LRUクロック + - field: executable + type: 1 + i18n: + zh-CN: 服务器执行路径 + en-US: Server's Executable Path + ja-JP: サーバーの実行パス + - field: config_file + type: 1 + i18n: + zh-CN: 配置文件路径 + en-US: Config File Path + ja-JP: 配置ファイルのパス + - field: io_threads_active + type: 0 + i18n: + zh-CN: 活跃IO线程数 + en-US: Active IO Threads + ja-JP: 活動中のI/Oスレッド数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + # metrics - clients + - name: clients + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + i18n: + zh-CN: 客户端信息 + en-US: Clients + ja-JP: クライアント情報 + # collect metrics content + fields: + - field: connected_clients + type: 0 + i18n: + zh-CN: 已连接客户端数量 + en-US: Connected Clients + ja-JP: 接続クライアント数 + - field: maxclients + type: 0 + i18n: + zh-CN: 最大客户端连接数 + en-US: Max Clients + ja-JP: 最大クライアント数 + - field: blocked_clients + type: 0 + i18n: + zh-CN: 阻塞客户端数量 + en-US: Blocked Clients + ja-JP: ブロックされたクライアント数 + - field: monitor_clients + type: 0 + i18n: + zh-CN: 监控的客户端数量 + en-US: monitor Clients + ja-JP: モニタークライアント数 + protocol: redis + redis: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + timeout: ^_^timeout^_^ + # metrics - memory + - name: memory + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + i18n: + zh-CN: 内存信息 + en-US: Memory + ja-JP: メモリ情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: used_memory_rss + type: 0 + i18n: + zh-CN: 已使用内存(字节) + en-US: Used Memory RSS + ja-JP: 使用した物理メモリ(バイト) + - field: used_memory_rss_human + type: 0 + unit: MB + i18n: + zh-CN: 已使用物理内存 + en-US: Used Memory RSS Human + ja-JP: 使用した物理メモリ + - field: used_memory_lua + type: 0 + i18n: + zh-CN: LUA脚本占用的内存(字节) + en-US: Used Memory LUA + ja-JP: LUAが使用するメモリ(バイト) + - field: used_memory_lua_human + type: 0 + unit: KB + i18n: + zh-CN: LUA脚本占用的内存 + en-US: Used Memory LUA Human + ja-JP: LUAが使用するメモリ + - field: used_memory_startup + type: 0 + i18n: + zh-CN: 启动占用内存 + en-US: Used Memory Startup + ja-JP: 起動時の使用メモリ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - persistence + - name: persistence + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + i18n: + zh-CN: 持久化信息 + en-US: Persistence + ja-JP: 永続化 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: loading + type: 0 + i18n: + zh-CN: 是否正在加载持久化文件 + en-US: Loading + ja-JP: 読み込み中 + - field: bgsave_in_progress + type: 0 + i18n: + zh-CN: 是否正在进行bgsave + en-US: bgsave In Progress + ja-JP: bgsaveである + - field: last_bgsave_time + type: 0 + i18n: + zh-CN: 最近一次bgsave命令执行时间 + en-US: Last Save Time + ja-JP: 最後のbgsave実行時間 + - field: last_bgsave_status + type: 1 + i18n: + zh-CN: 最近一次bgsave命令执行状态 + en-US: Last bgsave Status + ja-JP: 最後のbgsaveの実行状況 + - field: last_bgsave_time_sec + type: 0 + i18n: + zh-CN: 最近一次bgsave命令执行时间(秒) + en-US: Last bgsave Time Sec + ja-JP: 最後のbgsave実行時間(秒) + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - stats + - name: stats + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 4 + i18n: + zh-CN: 全局统计信息 + en-US: Stats + ja-JP: 統計情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: total_connections_received + type: 0 + i18n: + zh-CN: 已接受的总连接数 + en-US: Total Connections Received + ja-JP: 受信された接続数 + - field: total_commands_processed + type: 0 + i18n: + zh-CN: 执行过的命令总数 + en-US: Total Commands Processed + ja-JP: 処理済みのコマンド数 + - field: instantaneous_ops_per_sec + type: 0 + i18n: + zh-CN: 命令处理条数/秒 + en-US: Instantaneous Ops Per Sec + ja-JP: 処理されたコマンド数/秒 + - field: total_net_input_bytes + type: 0 + i18n: + zh-CN: 输入总网络流量(字节) + en-US: Total Net Input Bytes + ja-JP: 受信されたネットワークトラフィック(バイト) + - field: total_net_output_bytes + type: 0 + i18n: + zh-CN: 输出总网络流量(字节) + en-US: Total Net Output Bytes + ja-JP: 転送されたネットワークトラフィック(バイト) + - field: instantaneous_input_kbps + type: 0 + i18n: + zh-CN: 输入字节数/秒 + en-US: Instantaneous Input Kbps + ja-JP: 受信されたバイト/秒 + - field: instantaneous_output_kbps + type: 0 + i18n: + zh-CN: 输出字节数/秒 + en-US: Instantaneous Output Kbps + ja-JP: 転送されたバイト/秒 + - field: sync_full + type: 0 + i18n: + zh-CN: 主从完全同步成功次数 + en-US: Sync Full + ja-JP: Full Sync回数 + - field: sync_partial_ok + type: 0 + i18n: + zh-CN: 主从部分同步成功次数 + en-US: Sync Partial OK + ja-JP: Partial Sync成功回数 + - field: sync_partial_err + type: 0 + i18n: + zh-CN: 主从部分同步失败次数 + en-US: Sync Partial Error + ja-JP: Partial Sync失敗回数 + - field: pubsub_channels + type: 0 + i18n: + zh-CN: 订阅的频道数量 + en-US: Pubsub Channels + ja-JP: 購読されたチャンネル数 + - field: pubsub_patterns + type: 0 + i18n: + zh-CN: 订阅的模式数量 + en-US: Pubsub Patterns + ja-JP: 購読されたパターン数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - replication + - name: replication + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 5 + i18n: + zh-CN: 主从同步信息 + en-US: Replication + ja-JP: レプリケーション情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: role + type: 1 + i18n: + zh-CN: 节点角色 + en-US: Role + ja-JP: 役割 + - field: connected_slaves + type: 0 + i18n: + zh-CN: 已连接的从节点个数 + en-US: Connected Slaves + ja-JP: 接続スレーブ数 + - field: master_repl_offset + type: 0 + i18n: + zh-CN: 主节点偏移量 + en-US: Master Repl Offset + ja-JP: マスターのログオフセット + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - cpu + - name: cpu + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 6 + i18n: + zh-CN: CPU消耗信息 + en-US: CPU + ja-JP: CPU情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: used_cpu_sys + type: 0 + i18n: + zh-CN: Kvrocks进程使用的CPU时钟总和(内核态) + en-US: Used CPU Sys + ja-JP: Kvrocksが使用するシステム時間 + - field: used_cpu_user + type: 0 + i18n: + zh-CN: Kvrocks进程使用的CPU时钟总和(用户态) + en-US: Used CPU User + ja-JP: Kvrocksが使用するユーザー時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - Commandstats + - name: Commandstats + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 7 + i18n: + zh-CN: 命令信息 + en-US: Command Stats + ja-JP: コマンドの統計情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: cmdstat_command + type: 1 + i18n: + zh-CN: 命令 + en-US: Command Stat Command + ja-JP: コマンド + - field: cmdstat_info + type: 1 + i18n: + zh-CN: 命令监控信息 + en-US: Command Stat Info + ja-JP: コマンドの統計情報 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - cluster + - name: cluster + # collect metrics content + priority: 8 + i18n: + zh-CN: 集群信息 + en-US: Cluster + ja-JP: クラスター情報 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: cluster_enabled + type: 0 + i18n: + zh-CN: 节点是否开启集群模式 + en-US: Cluster Enabled + ja-JP: 有効 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - commandstats + - name: commandstats + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 9 + i18n: + zh-CN: 命令统计信息 + en-US: Command Stats + ja-JP: コマンドの統計情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: cmdstat_client + type: 1 + i18n: + zh-CN: 客户端命令统计 + en-US: cmdstat client + ja-JP: クライアントのコマンド + - field: cmdstat_config + type: 1 + i18n: + zh-CN: 配置命令统计 + en-US: cmdstat config + ja-JP: 配置のコマンド + - field: cmdstat_get + type: 1 + i18n: + zh-CN: get + en-US: get + ja-JP: get + - field: cmdstat_hello + type: 1 + i18n: + zh-CN: hello + en-US: hello + ja-JP: hello + - field: cmdstat_info + type: 1 + i18n: + zh-CN: info + en-US: info + ja-JP: info + - field: cmdstat_keys + type: 1 + i18n: + zh-CN: keys + en-US: keys + ja-JP: keys + - field: cmdstat_ping + type: 1 + i18n: + zh-CN: ping + en-US: ping + ja-JP: ping + - field: cmdstat_select + type: 1 + i18n: + zh-CN: select + en-US: select + ja-JP: select + - field: cmdstat_set + type: 1 + i18n: + zh-CN: set + en-US: set + ja-JP: set + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - keyspace + - name: keyspace + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 10 + i18n: + zh-CN: 数据库统计信息 + en-US: Keyspace + ja-JP: キー空間 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: db0 + type: 1 + i18n: + zh-CN: db0 + en-US: db0 + ja-JP: db0 + - field: sequence + type: 1 + i18n: + zh-CN: 序列 + en-US: sequence + ja-JP: シーケンス + - field: used_db_size + type: 1 + i18n: + zh-CN: 数据库使用大小 + en-US: used_db_size + ja-JP: 使用したサイズ + - field: max_db_size + type: 1 + i18n: + zh-CN: 数据库最大使用大小 + en-US: max_db_size + ja-JP: 最大サイズ + - field: used_percent + type: 1 + i18n: + zh-CN: 数据库使用百分比 + en-US: used_percent + ja-JP: パーセント + - field: disk_capacity + type: 1 + i18n: + zh-CN: 磁盘容量 + en-US: disk_capacity + ja-JP: ディスク容量 + - field: used_disk_size + type: 1 + i18n: + zh-CN: 占用磁盘大小 + en-US: used_disk_size + ja-JP: 使用したディスクサイズ + - field: used_disk_percent + type: 1 + i18n: + zh-CN: 占用磁盘百分比 + en-US: used_disk_percent + ja-JP: 使用したディスク率 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-linux.yml b/hertzbeat-manager/src/main/resources/define/app-linux.yml new file mode 100644 index 0000000..8f46be9 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-linux.yml @@ -0,0 +1,764 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: linux +# The monitoring i18n name +name: + zh-CN: Linux操作系统 + en-US: OS Linux + ja-JP: OS Linux +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 Linux 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Linux</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors Linux operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New Linux</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 Linux 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Linux</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> Linuxシステムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Linux</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/linux + en-US: https://hertzbeat.apache.org/docs/help/linux +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: (uname -r ; hostname ; uptime | awk -F "," '{print $1}' | sed "s/ //g") | sed ":a;N;s/\n/^/g;ta" | awk -F '^' 'BEGIN{print "version hostname uptime"} {print $1, $2, $3}' + # ssh response data parse type: oneRow, multiRow + parseType: multiRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100-idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: "LANG=C lscpu | awk -F: '$1==\"Model name\" {print $2}';awk '/processor/{core++} END{print core}' /proc/cpuinfo;uptime | sed 's/,/ /g' | awk '{for(i=NF-2;i<=NF;i++)print $i }' | xargs;vmstat 1 1 | awk 'NR==3{print $11}';vmstat 1 1 | awk 'NR==3{print $12}';vmstat 1 2 | awk 'NR==4{print $15}'" + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: free -m | awk 'BEGIN{print "total used free buff_cache available"} NR==2{print $2,$3,$4,$6,$7}' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + en-US: Partition Num + ja-JP: パーティション + - field: block_write + type: 0 + i18n: + zh-CN: 写磁盘块数 + en-US: Block Write + ja-JP: 書き込みディスクブロック数 + - field: block_read + type: 0 + i18n: + zh-CN: 读磁盘块数 + en-US: Block Read + ja-JP: 読み取りブロック数 + - field: write_rate + type: 0 + unit: iops + i18n: + zh-CN: 磁盘写速率 + en-US: Write Rate + ja-JP: ディスク書き込み速度 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: vmstat -D | awk 'NR==1{print $1}';vmstat -D | awk 'NR==2{print $1}';vmstat 1 1 | awk 'NR==3{print $10}';vmstat 1 1 | awk 'NR==3{print $9}';vmstat 1 1 | awk 'NR==3{print $16}' + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: cat /proc/net/dev | tail -n +3 | awk 'BEGIN{ print "interface_name receive_bytes transmit_bytes"} {print $1,$2,$10}' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -mP | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-linux_script.yml b/hertzbeat-manager/src/main/resources/define/app-linux_script.yml new file mode 100644 index 0000000..3d5eed0 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-linux_script.yml @@ -0,0 +1,445 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: linux_script +# The monitoring i18n name +name: + zh-CN: Linux 命令 + en-US: Linux Script + ja-JP: Linux Script +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用采集器作为 agent 直接运行 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-script'> Shell 命令 </a> 对 Linux 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Linux Script</i>”进行添加。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat uses a collector as an agent to directly execute <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-script'> Shell commands </a> to collect and monitor general performance metrics of the Linux operating system (system information, CPU, memory, disk, network card, file system, TOP resource processes, etc.).<br>You can click “<i>Create New Linux Script</i>” to add it. Or select “<i>More Actions</i>” to import an existing configuration. + zh-TW: Hertzbeat 使用采集器作為 agent 直接運行 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-script'> Shell 命令 </a> 對 Linux 操作系統的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Linux Script</i>”進行添加。或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は agent として <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-script'> Shell コマンドを介して </a> Linuxシステムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Linux Script</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/linux_script + en-US: https://hertzbeat.apache.org/docs/help/linux_script +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: script + # the config content when protocol is ssh + script: + # script tool + scriptTool: bash + # OS charset + charset: UTF-8 + # script working directory + workDirectory: /usr/bin + # collect script + scriptCommand: (uname -r ; hostname ; uptime | awk -F "," '{print $1}' | sed "s/ //g") | sed ":a;N;s/\n/^/g;ta" | awk -F '^' 'BEGIN{print "version hostname uptime"} {print $1, $2, $3}' + # response data parse type: oneRow, multiRow + parseType: multiRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100-idle + protocol: script + script: + scriptTool: bash + charset: UTF-8 + scriptCommand: + | + LANG=C lscpu | awk -F: '$1=="Model name" {gsub(/^ */, "", $2); print $2}';awk '/processor/{core++} END{print core}' /proc/cpuinfo;uptime | sed 's/,/ /g' | awk '{for(i=NF-2;i<=NF;i++)print $i }' | xargs;vmstat 1 1 | awk 'NR==3{print $11}';vmstat 1 1 | awk 'NR==3{print $12}';vmstat 1 2 | awk 'NR==4{print $15}' + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: script + script: + scriptTool: bash + charset: UTF-8 + scriptCommand: free -m | awk 'BEGIN{print "total used free buff_cache available"} NR==2{print $2,$3,$4,$6,$7}' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + en-US: Partition Num + ja-JP: パーティション + - field: block_write + type: 0 + i18n: + zh-CN: 写磁盘块数 + en-US: Block Write + ja-JP: 書き込みディスクブロック数 + - field: block_read + type: 0 + i18n: + zh-CN: 读磁盘块数 + en-US: Block Read + ja-JP: 読み取りブロック数 + - field: write_rate + type: 0 + unit: iops + i18n: + zh-CN: 磁盘写速率 + en-US: Write Rate + ja-JP: ディスク書き込み速度 + protocol: script + script: + scriptTool: bash + charset: UTF-8 + scriptCommand: vmstat -D | awk 'NR==1{print $1}';vmstat -D | awk 'NR==2{print $1}';vmstat 1 1 | awk 'NR==3{print $10}';vmstat 1 1 | awk 'NR==3{print $9}';vmstat 1 1 | awk 'NR==3{print $16}' + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: script + script: + scriptTool: bash + charset: UTF-8 + scriptCommand: cat /proc/net/dev | tail -n +3 | awk 'BEGIN{ print "interface_name receive_bytes transmit_bytes"} {print $1,$2,$10}' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: script + script: + scriptTool: bash + charset: UTF-8 + scriptCommand: df -mP | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: script + script: + scriptTool: bash + charset: UTF-8 + scriptCommand: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: script + script: + scriptTool: bash + charset: UTF-8 + scriptCommand: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-lmstudio.yml b/hertzbeat-manager/src/main/resources/define/app-lmstudio.yml new file mode 100644 index 0000000..fcb22f0 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-lmstudio.yml @@ -0,0 +1,202 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: llm +# The monitoring type eg: linux windows tomcat mysql aws... +app: lmstudio +# The monitoring i18n name +name: + zh-CN: LM Studio 监控 + en-US: LM Studio + ja-JP: LM Studio +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 将调用 LM Studio REST API 接口,监测本地模型列表数据。<br>您可以先点击"新增 LM Studio"按钮并进行配置,或在"更多操作"中导入已有配置。 + en-US: HertzBeat will call the LM Studio REST API to monitor local models.<br>You can click the "Add LM Studio" button to configure it first, or import existing configurations in the "More Actions" section. + zh-TW: HertzBeat 將調用 LM Studio REST API 接口,監測本地模型列表數據。<br>您可以先點擊"新增 LM Studio"按鈕並進行配置,或在"更多操作"中導入已有配置。 + ja-JP: HertzBeatは、LM Studio REST APIを呼び出してメトリクスを監視します。<br>「<i>新規 LM Studio</i>」をクリックして設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/lmstudio + en-US: https://hertzbeat.apache.org/docs/help/lmstudio +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: ターゲットホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + # when type is number, range is required + range: '[0,65535]' + required: true + defaultValue: 1234 + - field: ssl + name: + zh-CN: 启用HTTPS + en-US: HTTPS + ja-JP: HTTPS + type: boolean + required: false + defaultValue: false + - field: token + name: + zh-CN: API Token + en-US: API Token + ja-JP: APIトークン + type: text + required: false +# collect metrics config list +metrics: + # metrics - models + - name: models + # metrics name i18n label + i18n: + zh-CN: 模型列表 + en-US: Models + ja-JP: モデル一覧 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, i18n-metric name i18n label, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: key + type: 1 + i18n: + zh-CN: 模型标识 + en-US: Model Key + ja-JP: モデルキー + label: true + - field: display_name + type: 1 + i18n: + zh-CN: 模型名称 + en-US: Display Name + ja-JP: 表示名 + - field: type + type: 1 + i18n: + zh-CN: 模型类型 + en-US: Model Type + ja-JP: モデルタイプ + - field: publisher + type: 1 + i18n: + zh-CN: 发布者 + en-US: Publisher + ja-JP: パブリッシャー + - field: architecture + type: 1 + i18n: + zh-CN: 架构 + en-US: Architecture + ja-JP: アーキテクチャ + - field: quantization_name + type: 1 + i18n: + zh-CN: 量化方式 + en-US: Quantization + ja-JP: 量子化 + - field: quantization_bits + type: 0 + i18n: + zh-CN: 量化位数 + en-US: Quantization Bits + ja-JP: 量子化ビット数 + - field: size_bytes + type: 0 + i18n: + zh-CN: 模型大小 + en-US: Model Size + ja-JP: モデルサイズ + unit: MB + - field: params_string + type: 1 + i18n: + zh-CN: 参数量 + en-US: Parameters + ja-JP: パラメータ数 + - field: max_context_length + type: 0 + i18n: + zh-CN: 最大上下文长度 + en-US: Max Context Length + ja-JP: 最大コンテキスト長 + - field: format + type: 1 + i18n: + zh-CN: 模型格式 + en-US: Format + ja-JP: フォーマット + aliasFields: + - $.key + - $.display_name + - $.type + - $.publisher + - $.architecture + - $.quantization.name + - $.quantization.bits_per_weight + - $.size_bytes + - $.params_string + - $.max_context_length + - $.format + calculates: + - key=$.key + - display_name=$.display_name + - type=$.type + - publisher=$.publisher + - architecture=$.architecture + - quantization_name=$.quantization.name + - quantization_bits=$.quantization.bits_per_weight + - size_bytes=$.size_bytes + - params_string=$.params_string + - max_context_length=$.max_context_length + - format=$.format + units: + - size_bytes=B->MB + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /api/v1/models + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Bearer Token + bearerTokenToken: ^_^token^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$.models[*]' diff --git a/hertzbeat-manager/src/main/resources/define/app-macos.yml b/hertzbeat-manager/src/main/resources/define/app-macos.yml new file mode 100644 index 0000000..be8098f --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-macos.yml @@ -0,0 +1,750 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: macos +# The monitoring i18n name +name: + zh-CN: macOS + en-US: macOS + ja-JP: macOS +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 macOS 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 macOS</i>”并配置 HOST 端口账户等相关参数进行添加,支持 SSH 账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors macOS operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New macOS</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 macOS 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 macOS</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> macOS システムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 macOS</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/macos/ + en-US: https://hertzbeat.apache.org/docs/help/macos/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: scutil --get ComputerName; echo "$(sw_vers -productName) $(sw_vers -productVersion) ($(sw_vers -buildVersion))"; uptime| awk '{print $1,$2,$3,$4}' | sed 's/,//' + # ssh response data parse type: oneRow, multiRow + parseType: oneRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - usage=idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: sysctl -n machdep.cpu.brand_string; sysctl -n hw.ncpu; sysctl -n vm.loadavg | awk '{print $2,$3,$4}'; top -l 1 | grep 'CPU usage' | awk '{print $3}' | sed 's/%//' + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: echo "total used free buff_cache available"; vm_stat | awk 'BEGIN{cmd="sysctl -n hw.memsize";cmd|getline total;close(cmd);cmd="sysctl -n hw.pagesize";cmd|getline ps;close(cmd);total=total/1024/1024} /Pages free/{free=$3} /Pages active/{active=$3} /Pages inactive/{inactive=$3} /Pages wired/{wired=$4} /Pages occupied by compressor/{comp=$5} END{free_mb=free*ps/1024/1024;used_mb=(active+inactive+wired+comp)*ps/1024/1024;inactive_mb=inactive*ps/1024/1024;avail_mb=(free+inactive)*ps/1024/1024;printf "%.0f %.0f %.0f %.0f %.0f\n",total,used_mb,free_mb,inactive_mb,avail_mb}' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + en-US: Partition Num + ja-JP: パーティション + - field: block_write + type: 0 + i18n: + zh-CN: 写磁盘块数 + en-US: Block Write + ja-JP: 書き込みディスクブロック数 + - field: block_read + type: 0 + i18n: + zh-CN: 读磁盘块数 + en-US: Block Read + ja-JP: 読み取りブロック数 + - field: write_rate + type: 0 + unit: iops + i18n: + zh-CN: 磁盘写速率 + en-US: Write Rate + ja-JP: ディスク書き込み速度 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: iostat -d | awk 'NR==3{print $1}'; iostat -d | awk 'NR==3{print $2}'; vm_stat | awk '/Pages free/{print $3}' | tr -d ':'; vm_stat | awk '/Pages active/{print $3}' | tr -d ':'; sysctl -n hw.memsize | awk '{print int($1/1024/1024)}' + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: netstat -ibn | awk 'NR==1{print "interface_name receive_bytes transmit_bytes"} NR>1 && $1!~/Name/ && $7~/^[0-9]+$/{print $1,$7,$10}' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -m | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$9}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } NR>1{cmd=""; for(i=11;i<=NF;i++) cmd=cmd $i (i==NF?"":" "); printf "%s %s %s %s\n", $2, $3, $4, cmd}' | head -n 11 + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-mariadb.yml b/hertzbeat-manager/src/main/resources/define/app-mariadb.yml new file mode 100644 index 0000000..73cd3c4 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-mariadb.yml @@ -0,0 +1,935 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: mariadb +# The monitoring i18n name +name: + zh-CN: MariaDB数据库 + en-US: MariaDB + ja-JP: MariaDB データベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 MariaDB 数据库的通用性能指标(系统信息、性能状态、Innodb、缓存、事物、用户线程、慢SQL等)进行采集监控,支持版本为 MariaDB5+。<br>您可以点击“<i>新建 MariaDB 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC Protocol</a> to configure SQL for collecting general metrics of MariaDB database (system information, performance status, Innodb, cache, things, user threads, slow SQL, etc.). Supported version is MariaDB5+. <br>You can click "<i>New MariaDB Database</i>" and configure it, or select "<i>More Operations</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 協議</a> 通過配置 SQL 對 MariaDB 數據庫的通用性能指標(系統信息、性能狀態、Innodb、緩存、事物、用戶線程、慢SQL等)進行采集監控,支持版本爲 MariaDB5+。<br>您可以點擊“<i>新建 MariaDB 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBCプロトコルを介して</a> MariaDB データベース(MariaDB5+)の一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 MariaDB データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/mariadb + en-US: https://hertzbeat.apache.org/docs/help/mariadb +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 3306 + - field: timeout + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + type: number + # when type is number, range is required + range: '[400,200000]' + required: false + defaultValue: 6000 + hide: true + - field: database + name: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + type: text + required: false + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + - field: url + name: + zh-CN: URL + en-US: URL + ja-JP: URL + type: text + required: false + hide: true + +# collect metrics config list +metrics: + # metrics - basic + - name: basic + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + i18n: + zh-CN: 基础信息 + en-US: Basic Info + ja-JP: 基礎情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + - field: port + type: 1 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: datadir + type: 1 + i18n: + zh-CN: 存储目录 + en-US: Data Directory + ja-JP: データディレクトリ + - field: max_connections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + - field: thread_cache_size + type: 0 + i18n: + zh-CN: 连接池大小 + en-US: Thread Cache Size + ja-JP: キャッシュするスレッドの数 + - field: innodb_buffer_pool_size + type: 0 + unit: KB + i18n: + zh-CN: InnoDB缓冲池的大小 + en-US: Innodb Buffer Pool Size + ja-JP: Innodbバッファープールのサイズ + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - version + - version_compile_os + - version_compile_machine + - port + - datadir + - max_connections + - thread_cache_size + - table_open_cache + - innodb_buffer_pool_size + # (optional)mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - port=port + - datadir=datadir + - max_connections=max_connections + - version=version+"_"+version_compile_os+"_"+version_compile_machine + # Convert the initial unit of the metric to the final unit + units: + - innodb_buffer_pool_size=B->KB + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # mysql host: ipv4 ipv6 host + host: ^_^host^_^ + # mysql port + port: ^_^port^_^ + # database platform name + platform: mariadb + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # database name + database: ^_^database^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + # sql + sql: show global variables where Variable_name like 'version%' or Variable_name = 'max_connections' or Variable_name = 'datadir' or Variable_name = 'port' or Variable_name = 'thread_cache_size' or Variable_name = 'table_open_cache' or Variable_name = 'innodb_buffer_pool_size'; + # JDBC url + url: ^_^url^_^ + + - name: cache + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + i18n: + zh-CN: 缓存信息 + en-US: Cache Info + ja-JP: キャッシュ情報 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: query_cache_hit_rate + type: 0 + unit: '%' + i18n: + zh-CN: 缓存命中率 + en-US: Query Cache Hit Rate + ja-JP: キャッシュ命中率 + - field: cache_hits + type: 0 + i18n: + zh-CN: 缓存命中数 + en-US: Cache Hits + ja-JP: キャッシュ命中数 + - field: cache_inserts + type: 0 + i18n: + zh-CN: 缓存插入数 + en-US: Cache Inserts + ja-JP: キャッシュインサート数 + - field: cache_free_blocks + type: 0 + i18n: + zh-CN: 缓存空闲块数量 + en-US: Cache Free Blocks + ja-JP: キャッシュの空きブロック数 + - field: cache_free_memory + type: 0 + unit: KB + i18n: + zh-CN: 空闲缓存大小 + en-US: Cache Free Memory + ja-JP: キャッシュの利用可能メモリ + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - Qcache_hits + - Qcache_inserts + - Qcache_free_blocks + - Qcache_free_memory + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - query_cache_hit_rate= (Qcache_hits + 1) / (Qcache_hits + Qcache_inserts + 1) * 100 + - cache_hits=Qcache_hits + - cache_inserts=Qcache_inserts + - cache_free_blocks=Qcache_free_blocks + - cache_free_memory=Qcache_free_memory + # Convert the initial unit of the metric to the final unit + units: + - cache_free_memory=B->KB + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # jdbc host + host: ^_^host^_^ + # port + port: ^_^port^_^ + # platform name + platform: mariadb + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # database name + database: ^_^database^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + # sql + sql: show global status like 'QCache%'; + url: ^_^url^_^ + + - name: performance + priority: 2 + i18n: + zh-CN: 性能信息 + en-US: Performance Info + ja-JP: パフォーマンス情報 + fields: + - field: questions + type: 0 + i18n: + zh-CN: 查询总数 + en-US: Questions + ja-JP: クエリ総数 + - field: qps + type: 0 + i18n: + zh-CN: QPS + en-US: QPS + ja-JP: QPS + aliasFields: + - uptime + - questions + calculates: + - qps=uptime / questions + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mariadb + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name = 'questions' or Variable_name = 'uptime'; + url: ^_^url^_^ + + - name: innodb + priority: 3 + i18n: + zh-CN: Innodb信息 + en-US: Innodb Info + ja-JP: Innodb情報 + fields: + - field: innodb_data_reads + type: 0 + unit: Times + i18n: + zh-CN: 磁盘读取次数 + en-US: Innodb Data Reads + ja-JP: Innodb読み取り回数 + - field: innodb_data_writes + type: 0 + unit: Times + i18n: + zh-CN: 磁盘写入次数 + en-US: Innodb Data Writes + ja-JP: Innodb書き込み回数 + - field: innodb_data_read + type: 0 + unit: KB + i18n: + zh-CN: 磁盘读取数据量 + en-US: Innodb Data Read + ja-JP: Innodb読み取り(KB) + - field: innodb_data_written + type: 0 + unit: KB + i18n: + zh-CN: 磁盘写入数据量 + en-US: Innodb Data Written + ja-JP: Innodb書き込み(KB) + - field: innodb_buffer_hit_rate + type: 0 + unit: '%' + i18n: + zh-CN: Innodb 缓存命中数 + en-US: Innodb Buffer Hit Rate + ja-JP: Innodbバッファーキャッシュ命中率 + aliasFields: + - Innodb_data_reads + - Innodb_data_writes + - Innodb_data_read + - Innodb_data_written + - Innodb_buffer_pool_read_requests + - Innodb_buffer_pool_read_ahead + - Innodb_buffer_pool_reads + calculates: + - innodb_buffer_hit_rate= (Innodb_buffer_pool_read_requests + 1) / (Innodb_buffer_pool_read_requests + Innodb_buffer_pool_read_ahead + Innodb_buffer_pool_reads + 1) * 100 + - innodb_data_reads=Innodb_data_reads + - innodb_data_writes=Innodb_data_writes + - innodb_data_read=Innodb_data_read + - innodb_data_written=Innodb_data_written + units: + - innodb_data_read=B->KB + - innodb_data_written=B->KB + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mariadb + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name like 'innodb%'; + url: ^_^url^_^ + + - name: status + priority: 4 + i18n: + zh-CN: 状态信息 + en-US: Status Info + ja-JP: 状態情報 + fields: + - field: uptime + type: 0 + unit: s + i18n: + zh-CN: 运行时间 + en-US: Uptime + ja-JP: アップタイム + - field: com_select + type: 0 + i18n: + zh-CN: 查询次数 + en-US: Com Select + ja-JP: セレクト回数 + - field: com_insert + type: 0 + i18n: + zh-CN: 插入次数 + en-US: Com Insert + ja-JP: インサート回数 + - field: com_update + type: 0 + i18n: + zh-CN: 更新次数 + en-US: Com Update + ja-JP: 更新回数 + - field: com_delete + type: 0 + i18n: + zh-CN: 删除次数 + en-US: Com Delete + ja-JP: 削除回数 + - field: com_commit + type: 0 + i18n: + zh-CN: 事务提交次数 + en-US: Com Commit + ja-JP: トランザクションのコミット数 + - field: com_rollback + type: 0 + i18n: + zh-CN: 事务回滚次数 + en-US: Com Rollback + ja-JP: トランザクションのロールバック数 + - field: threads_created + type: 0 + i18n: + zh-CN: 线程创建数 + en-US: Threads Created + ja-JP: スレッド作成数 + - field: threads_connected + type: 0 + i18n: + zh-CN: 连接线程数 + en-US: Threads Connected + ja-JP: 接続スレッド数 + - field: threads_cached + type: 0 + i18n: + zh-CN: 缓存线程数 + en-US: Threads Cached + ja-JP: キャッシュスレッド数 + - field: threads_running + type: 0 + i18n: + zh-CN: 活动线程数 + en-US: Threads Running + ja-JP: 活動中のスレッド数 + - field: qps + type: 0 + i18n: + zh-CN: QPS + en-US: QPS + ja-JP: QPS + aliasFields: + - uptime + - com_select + - com_insert + - com_update + - com_delete + - com_commit + - com_rollback + - threads_created + - threads_connected + - threads_cached + - threads_running + - questions + calculates: + - qps=uptime / questions + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mariadb + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name like 'thread%' or Variable_name = 'com_select' or Variable_name = 'com_insert' or Variable_name = 'com_update' or Variable_name = 'com_delete' or Variable_name = 'com_commit' or Variable_name = 'com_rollback' or Variable_name = 'questions' or Variable_name = 'uptime'; + url: ^_^url^_^ + + - name: handler + priority: 5 + i18n: + zh-CN: 处理器信息 + en-US: Handler Info + ja-JP: ハンドラ情報 + fields: + - field: handler_commit + type: 0 + i18n: + zh-CN: 处理的事务提交数 + en-US: Handler Commit + ja-JP: ハンドラのコミット操作数 + - field: handler_delete + type: 0 + i18n: + zh-CN: 处理的删除操作数 + en-US: Handler Delete + ja-JP: ハンドラの削除操作数 + - field: handler_write + type: 0 + i18n: + zh-CN: 处理的写入操作数 + en-US: Handler Write + ja-JP: ハンドラの書き込み操作数 + - field: handler_update + type: 0 + i18n: + zh-CN: 处理的更新操作数 + en-US: Handler Update + ja-JP: ハンドラの更新操作数 + - field: handler_discover + type: 0 + i18n: + zh-CN: 处理的发现操作数 + en-US: Handler Discover + ja-JP: ハンドラのディスカバー操作数 + - field: handler_prepare + type: 0 + i18n: + zh-CN: 处理的准备操作数 + en-US: Handler Prepare + ja-JP: ハンドラの準備操作数 + - field: handler_read_first + type: 0 + i18n: + zh-CN: 读取首行操作数 + en-US: Handler Read First + ja-JP: ハンドラの最初の行を読む操作数 + - field: handler_read_rnd + type: 0 + i18n: + zh-CN: 随机读取操作数 + en-US: Handler Read RND + ja-JP: ハンドラのランダムアクセス操作数 + - field: handler_read_next + type: 0 + i18n: + zh-CN: 读取下一行操作数 + en-US: Handler Read Next + ja-JP: ハンドラの次の行を読む操作数 + - field: handler_read_rnd_next + type: 0 + i18n: + zh-CN: 随机读取下一行操作数 + en-US: Handler Read RND Next + ja-JP: ハンドラの次の行をランダムに読む操作数 + - field: handler_read_key + type: 0 + i18n: + zh-CN: 按索引键读取操作数 + en-US: Handler Read Key + ja-JP: ハンドラのインデックスを使用して読み取る操作数 + - field: handler_external_lock + type: 0 + i18n: + zh-CN: 外部锁定操作数 + en-US: Handler External Key + ja-JP: ハンドラの外部のキーを使用して読み取る操作数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mariadb + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status like 'Handler%'; + url: ^_^url^_^ + + - name: connection + priority: 6 + i18n: + zh-CN: 连接信息 + en-US: Connection Info + ja-JP: 接続情報 + fields: + - field: connections + type: 0 + i18n: + zh-CN: 当前连接数 + en-US: Connections + ja-JP: 現在接続数 + - field: max_used_connections + type: 0 + i18n: + zh-CN: 历史最大连接数 + en-US: Max Used Connections + ja-JP: 最大接続数 + - field: aborted_connects + type: 0 + i18n: + zh-CN: 失败连接数 + en-US: Aborted Connects + ja-JP: 失敗接続数 + - field: aborted_clients + type: 0 + i18n: + zh-CN: 客户端中断数 + en-US: Aborted Clients + ja-JP: クライアントの割り込み数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mariadb + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status; + url: ^_^url^_^ + + - name: thread + priority: 7 + i18n: + zh-CN: 线程信息 + en-US: Thread Info + ja-JP: スレッド情報 + fields: + - field: threads_created + type: 0 + i18n: + zh-CN: 创建线程数 + en-US: Threads Created + ja-JP: スレッド作成数 + - field: threads_connected + type: 0 + i18n: + zh-CN: 连接线程数 + en-US: Threads Connected + ja-JP: 接続スレッド数 + - field: threads_cached + type: 0 + i18n: + zh-CN: 缓存线程数 + en-US: CThreads Cached + ja-JP: キャッシュスレッド数 + - field: threads_running + type: 0 + i18n: + zh-CN: 运行线程数 + en-US: Threads Running + ja-JP: 活動中のスレッド数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mariadb + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status like 'thread%'; + url: ^_^url^_^ + + - name: tmp + priority: 8 + i18n: + zh-CN: 临时表信息 + en-US: TMP Info + ja-JP: 一時テーブル情報 + fields: + - field: created_tmp_tables + type: 0 + i18n: + zh-CN: 创建的临时表数量 + en-US: Created TMP Tables + ja-JP: 一時テーブル作成数 + - field: created_tmp_disk_tables + type: 0 + i18n: + zh-CN: 创建的临时磁盘表数量 + en-US: Created TMP Disk Tables + ja-JP: 一時ディスクテーブル作成数 + - field: created_tmp_files + type: 0 + i18n: + zh-CN: 创建的临时文件数量 + en-US: Created TMP Files + ja-JP: 一時ファイル作成数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mariadb + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name like '%tmp%'; + url: ^_^url^_^ + + - name: select_type + priority: 9 + i18n: + zh-CN: 查询类型信息 + en-US: Select Type Info + ja-JP: 検索タイプ情報 + fields: + - field: select_full_join + type: 0 + i18n: + zh-CN: 全表连接数 + en-US: Select Full Join + ja-JP: 全件結合数 + - field: select_full_range_join + type: 0 + i18n: + zh-CN: 带范围的全表连接数 + en-US: Select Full Range Join + ja-JP: 範囲検索付き全件結合数 + - field: select_range + type: 0 + i18n: + zh-CN: 索引范围查询数 + en-US: Select Range + ja-JP: 範囲検索数 + - field: select_range_check + type: 0 + i18n: + zh-CN: 索引范围内二次检查数 + en-US: Select Range Check + ja-JP: 範囲内二次検索数 + - field: select_scan + type: 0 + i18n: + zh-CN: 全表扫描查询数 + en-US: Select Scan + ja-JP: テーブル全体をスキャンする回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mariadb + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name like 'select%'; + url: ^_^url^_^ + + - name: sort + priority: 10 + i18n: + zh-CN: 排序信息 + en-US: Sort Info + ja-JP: ソート情報 + fields: + - field: sort_rows + type: 0 + i18n: + zh-CN: 排序总行数 + en-US: Sort Rows + ja-JP: ソートされた行数 + - field: sort_range + type: 0 + i18n: + zh-CN: 范围条件排序次数 + en-US: Sort Range + ja-JP: 範囲付きのソート回数 + - field: sort_merge_passes + type: 0 + i18n: + zh-CN: 合并排序段次数 + en-US: Sort Merge Passes + ja-JP: マージソート回数 + - field: sort_scan + type: 0 + i18n: + zh-CN: 全表扫描排序次数 + en-US: Sort Scan + ja-JP: テーブル全体をスキャンしてソートする回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mariadb + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name like 'sort%'; + url: ^_^url^_^ + + - name: table_lock + priority: 11 + i18n: + zh-CN: 表级锁信息 + en-US: Table Lock Info + ja-JP: テーブルロック情報 + fields: + - field: table_locks_immediate + type: 0 + i18n: + zh-CN: 立即获得表锁次数 + en-US: Table Locks Immediate + ja-JP: 即座に取得できた回数 + - field: table_locks_waited + type: 0 + i18n: + zh-CN: 须等待获取表锁次数 + en-US: Table Locks Waited + ja-JP: 取得を待機した回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mariadb + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name like 'table_lock%'; + url: ^_^url^_^ + + - name: process_state + priority: 12 + i18n: + zh-CN: 进程状态信息 + en-US: Process State Info + ja-JP: プロセス状態情報 + fields: + - field: state + type: 1 + label: true + i18n: + zh-CN: 进程状态 + en-US: State + ja-JP: プロセス状態 + - field: num + type: 0 + i18n: + zh-CN: 该状态进程数量 + en-US: Num + ja-JP: プロセス総数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mariadb + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: multiRow + sql: select state, count(*) as num from information_schema.PROCESSLIST where state != '' group by state; + url: ^_^url^_^ + + - name: slow_sql + priority: 13 + i18n: + zh-CN: 慢查询信息 + en-US: Slow Sql Info + ja-JP: スローSQL情報 + fields: + - field: id + type: 1 + label: true + i18n: + zh-CN: ID + en-US: ID + ja-JP: ID + - field: sql_text + type: 1 + i18n: + zh-CN: SQL 文本 + en-US: SQL Text + ja-JP: SQL文のテキスト + - field: start_time + type: 1 + i18n: + zh-CN: 开始时间 + en-US: Start Time + ja-JP: 開始時間 + - field: db + type: 1 + i18n: + zh-CN: 数据库 + en-US: Database + ja-JP: データベース + - field: query_time + type: 3 + i18n: + zh-CN: 查询时间 + en-US: Query Time + ja-JP: クエリ時間 + aliasFields: + - sql_text + - start_time + - db + - query_time + calculates: + - id= start_time + sql_text + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mariadb + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: multiRow + sql: select sql_text, start_time, db, query_time from mysql.slow_log; + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-memcached.yml b/hertzbeat-manager/src/main/resources/define/app-memcached.yml new file mode 100644 index 0000000..72d915f --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-memcached.yml @@ -0,0 +1,184 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: cache +# The monitoring type eg: linux windows tomcat mysql aws... +app: memcached +# monitoring i18n name +name: + zh-CN: Memcached + en-US: Memcached + ja-JP: Memcached +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 支持 Memcached 服务监控,通过(stats、stats settings、stats sizes)命令对(服务器进程ID、服务器已运行秒数、版本、当前线程数、item的大小、item的数量、当前存储占用的字节数)等指标进行监测。 + en-US: HertzBeat supports Memcached service monitoring by using commands such as stats, stats settings, and stats sizes to monitor indicators such as server process ID, server running seconds, version, current number of threads, item size, number of items, and current byte storage usage. + zh-TW: HertzBeat支持Memcached服務監控,通過(stats、stats settings、stats sizes)命令對(服務器行程ID、服務器已運行秒數、版本、當前線程數、item的大小、item的數量、當前存儲佔用的位元組數)等名額進行監測。 + ja-JP: HertzBeat は Memcached サービスの一般的なメトリクスを監視します。 +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 11211 +# collect metrics config list +metrics: + # metrics - summary + - name: server_info + i18n: + zh-CN: 系统信息 + en-US: Server Info + ja-JP: サーバー情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: pid + type: 1 + i18n: + zh-CN: 服务器进程ID + en-US: PID + ja-JP: プロセスID + - field: uptime + type: 0 + unit: s + i18n: + zh-CN: 已运行秒数 + en-US: Uptime + ja-JP: アップタイム + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + - field: curr_connections + type: 0 + i18n: + zh-CN: 当前连接数量 + en-US: Curr Connections + ja-JP: 現在接続数 + - field: auth_errors + type: 0 + i18n: + zh-CN: 认证失败数目 + en-US: Auth Errors + ja-JP: 認証失敗回数 + - field: threads + type: 0 + i18n: + zh-CN: 当前线程数 + en-US: Threads + ja-JP: 現在スレッド数 + - field: item_size + type: 0 + unit: byte + i18n: + zh-CN: Item 大小 + en-US: Item Size + ja-JP: Itemサイズ + - field: item_count + type: 0 + i18n: + zh-CN: Item 数量 + en-US: Item Count + ja-JP: Item数量 + - field: curr_items + type: 0 + i18n: + zh-CN: 当前存储的数据总数 + en-US: Curr Items + ja-JP: 現在のItem数 + - field: total_items + type: 0 + i18n: + zh-CN: 启动以来存储的数据总数 + en-US: Total Items + ja-JP: Item総数 + - field: bytes + type: 0 + unit: byte + i18n: + zh-CN: 当前存储占用的字节数 + en-US: Bytes + ja-JP: バイト + - field: cmd_get + type: 0 + i18n: + zh-CN: Get命令请求次数 + en-US: Cmd Get + ja-JP: Getコマンドの実行回数 + - field: cmd_set + type: 0 + i18n: + zh-CN: Set命令请求次数 + en-US: Cmd Set + ja-JP: Setコマンドの実行回数 + - field: cmd_flush + type: 0 + i18n: + zh-CN: Flush命令请求次数 + en-US: Cmd Flush + ja-JP: Flushコマンドの実行回数 + - field: get_misses + type: 0 + i18n: + zh-CN: Get命令未命中次数 + en-US: Get Misses + ja-JP: Getコマンドのミス回数 + - field: delete_misses + type: 0 + i18n: + zh-CN: Delete命令未命中次数 + en-US: Delete Misses + ja-JP: Deleteコマンドのミス回数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: memcached + # Specific collection configuration when protocol is telnet protocol + memcached: + # telnet host + host: ^_^host^_^ + port: ^_^port^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-modbus.yml b/hertzbeat-manager/src/main/resources/define/app-modbus.yml new file mode 100644 index 0000000..289a1b3 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-modbus.yml @@ -0,0 +1,246 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: modbus +# The app api i18n name +name: + zh-CN: ModBus服务器 + en-US: ModBus Server + ja-JP: ModBusサーバー +# The description and help of this monitoring type +help: + zh-CN: HertzBeat对支持Modbus协议的服务进行(保持寄存器和线圈)相关指标进行采集 + en-US: HertzBeat collects metrics related to maintaining registers and coils for services that support Modbus protocol + zh-TW: HertzBeat對支持Modbus協定的服務進行(保持寄存器和線圈)相關名額進行採集 + ja-JP: HertzBeatはModbusプロトコルをサポートするサービスのメトリクスを監視します。 +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: ModBus服务Host + en-US: ModBus Server Host + ja-JP: ModBusサーバーのホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 502 + - field: slaveId + # name-param field display i18n name + name: + zh-CN: slaveId + en-US: slaveId + ja-JP: slaveId + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + defaultValue: 1 + - field: timeout + name: + zh-CN: 请求超时时间(ms) + en-US: Request Timeout(ms) + ja-JP: リクエストタイムアウト(ms) + type: number + # when type is number, range is required + range: '[400,200000]' + required: false + defaultValue: 6000 + hide: true + - field: holdingRegisterAddresses + # name-param field display i18n name + name: + zh-CN: 保持寄存器地址 + en-US: Holding Registers address + ja-JP: Holdingレジスタのアドレス + # type-param field type(most mapping the html input type) + type: array + # param field input placeholder + placeholder: 'Input RegisterAddress' + # required-true or false + required: true + - field: coilRegisterAddresses + # name-param field display i18n name + name: + zh-CN: 线圈寄存器地址 + en-US: Coil Register address + ja-JP: コイルレジスタのアドレス + # type-param field type(most mapping the html input type) + type: array + # param field input placeholder + placeholder: 'Input RegisterAddress' + # required-true or false + required: true + +# collect metrics config list +metrics: + # metrics - summary + - name: holding-register + i18n: + zh-CN: 保持寄存器 统计信息 + en-US: holding-register stats + ja-JP: Holdingレジスタの統計情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: address-0 + type: 0 + i18n: + zh-CN: address-0 + en-US: address-0 + ja-JP: address-0 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: address-1-0 + type: 0 + i18n: + zh-CN: address-1-0 + en-US: address-1-0 + ja-JP: address-1-0 + - field: address-1-1 + type: 0 + i18n: + zh-CN: address-1-1 + en-US: address-1-1 + ja-JP: address-1-1 + - field: address-1-2 + type: 0 + i18n: + zh-CN: address-1-2 + en-US: address-1-2 + ja-JP: address-1-2 + aliasFields: + - responseTime + - holding-register:0 + - holding-register:1-0 + - holding-register:1-1 + - holding-register:1-2 + # mapping and conversion expressions, use the same aliasField above to calculate metrics value# eg: cores=core1+core2, usage=usage, waitTimeallTime-runningTime + calculates: + - responseTime=responseTime + - address-0=holding-register:0 + - address-1-0=holding-register:1-0 + - address-1-1=holding-register:1-1 + - address-1-2=holding-register:1-2 + protocol: modbus + # the config content when protocol is http + modbus: + # host + host: ^_^host^_^ + # port + port: ^_^port^_^ + driverName: modbus-tcp + addressSyntax: holding-register + slaveId: ^_^slaveId^_^ + timeout: ^_^timeout^_^ + registerAddresses: [ ^_^holdingRegisterAddresses^_^ ] + + # metrics - summary + - name: coil + i18n: + zh-CN: 线圈 统计信息 + en-US: coil stats + ja-JP: コイルの統計情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: address-0 + type: 0 + i18n: + zh-CN: address-0 + en-US: address-0 + ja-JP: address-0 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: address-1-0 + type: 0 + i18n: + zh-CN: address-1-0 + en-US: address-1-0 + ja-JP: address-1-0 + - field: address-1-1 + type: 0 + i18n: + zh-CN: address-1-1 + en-US: address-1-1 + ja-JP: address-1-1 + - field: address-1-2 + type: 0 + i18n: + zh-CN: address-1-2 + en-US: address-1-2 + ja-JP: address-1-2 + aliasFields: + - responseTime + - coil:0 + - coil:1-0 + - coil:1-1 + - coil:1-2 + # mapping and conversion expressions, use the same aliasField above to calculate metrics value# eg: cores=core1+core2, usage=usage, waitTimeallTime-runningTime + calculates: + - responseTime=responseTime + - address-0=coil:0 + - address-1-0=coil:1-0 + - address-1-1=coil:1-1 + - address-1-2=coil:1-2 + protocol: modbus + # the config content when protocol is http + modbus: + # host + host: ^_^host^_^ + # port + port: ^_^port^_^ + driverName: modbus-tcp + addressSyntax: coil + slaveId: ^_^slaveId^_^ + timeout: ^_^timeout^_^ + registerAddresses: [ ^_^coilRegisterAddresses^_^ ] \ No newline at end of file diff --git a/hertzbeat-manager/src/main/resources/define/app-mongodb.yml b/hertzbeat-manager/src/main/resources/define/app-mongodb.yml new file mode 100644 index 0000000..4edd66a --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-mongodb.yml @@ -0,0 +1,466 @@ +# 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. + + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: mongodb +# The monitoring i18n name +name: + zh-CN: MongoDB数据库 + en-US: MongoDB + ja-JP: MongoDBデータベース +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 MongoDB 数据库的通用指标进行测量监控。<br>您可以点击 “<i>新建 MongoDB 数据库</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors MongoDB general performance metrics. You could click the "<i>New MongoDB</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 MongoDB 數據庫的通用指標進行測量監控。<br>您可以點擊 “<i>新建 MongoDB 數據庫</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は MongoDBデータベースの一般的なメトリクスを監視します。<br>「<i>新規 MongoDBデータベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/mongodb + en-US: https://hertzbeat.apache.org/docs/help/mongodb +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 27017 + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + - field: database + name: + zh-CN: 数据库 + en-US: database + ja-JP: データベース + type: text + # when type is text, use limit to limit string length + limit: 50 + required: true + defaultValue: 'test' + - field: authenticationDatabase + name: + zh-CN: 认证数据库 + en-US: authenticationDatabase + ja-JP: 認証データベース + type: text + limit: 50 + required: true + defaultValue: 'admin' + - field: timeout + # name-param field display i18n name + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 +# can get status information and other config params of MongoDB by running Diagnostic Commands +# https://www.mongodb.com/docs/manual/reference/command/nav-diagnostic/ +# name must be Diagnostic Commands 's name,support . way to get subdocument, should be careful about if the user that need connection have the right to run command + +# collect metrics config list +metrics: + # https://www.mongodb.com/docs/manual/reference/command/buildInfo/#usage + # metrics - buildInfo + - name: buildInfo + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + i18n: + zh-CN: 构建信息 + en-US: Build Info + ja-JP: ビルド情報 + # monitoring metrics list + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + - field: gitVersion + type: 1 + i18n: + zh-CN: git版本 + en-US: Git Version + ja-JP: Gitバージョン + - field: sysInfo + type: 1 + i18n: + zh-CN: 系统信息 + en-US: Sys Info + ja-JP: システム情報 + - field: loaderFlags + type: 1 + i18n: + zh-CN: 加载器标志 + en-US: Loader Flags + ja-JP: ローダフラグ + - field: compilerFlags + type: 1 + i18n: + zh-CN: 编译器标志 + en-US: Compiler Flags + ja-JP: コンパイラフラグ + - field: allocator + type: 1 + i18n: + zh-CN: 分配器 + en-US: Allocator + ja-JP: メモリ割り当て器 + - field: javascriptEngine + type: 1 + i18n: + zh-CN: JavaScript引擎 + en-US: JavaScript Engine + ja-JP: JavaScriptエンジン + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: mongodb + # the config content when protocol is mongodb + mongodb: + #mongodb host: + host: ^_^host^_^ + #mongodb port + port: ^_^port^_^ + #user's username + username: ^_^username^_^ + #user's password + password: ^_^password^_^ + #mongodb's database + database: ^_^database^_^ + #Specifies the database in which the User is created + authenticationDatabase: ^_^authenticationDatabase^_^ + #The buildInfo command is an administrative command which returns a build summary for the current mongod. + command: buildInfo + # timeout unit:ms + timeout: ^_^timeout^_^ + + # https://www.mongodb.com/docs/manual/reference/command/serverStatus/#metrics + - name: server_document + priority: 1 + i18n: + zh-CN: 服务器文档 + en-US: Server Document + ja-JP: サーバードキュメント + fields: + - field: deleted + type: 0 + i18n: + zh-CN: 已删除数 + en-US: Deleted + ja-JP: 削除回数 + - field: inserted + type: 0 + i18n: + zh-CN: 已插入数 + en-US: Inserted + ja-JP: インサート回数 + - field: returned + type: 0 + i18n: + zh-CN: 返回结果数 + en-US: Returned + ja-JP: 戻る回数 + - field: updated + type: 0 + i18n: + zh-CN: 已更新数 + en-US: Updated + ja-JP: 更新回数 + protocol: mongodb + mongodb: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + authenticationDatabase: ^_^authenticationDatabase^_^ + command: serverStatus.metrics.document + timeout: ^_^timeout^_^ + + - name: server_operation + priority: 2 + i18n: + zh-CN: 服务器操作 + en-US: Server Operation + ja-JP: サーバー操作 + fields: + - field: scanAndOrder + type: 0 + i18n: + zh-CN: 扫描订购 + en-US: Scan And Order + ja-JP: スキャンとオーダー + - field: writeConflicts + type: 0 + i18n: + zh-CN: 写冲突 + en-US: Write And Order + ja-JP: 書き込みとオーダー + protocol: mongodb + mongodb: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + authenticationDatabase: ^_^authenticationDatabase^_^ + command: serverStatus.metrics.operation + timeout: ^_^timeout^_^ + + - name: server_ttl + priority: 3 + i18n: + zh-CN: 服务器_TTL + en-US: server TTL + ja-JP: サーバーTTL + fields: + - field: deletedDocuments + type: 0 + i18n: + zh-CN: 已删除文档数 + en-US: Deleted Documents + ja-JP: 削除されたドキュメント数 + - field: passes + type: 0 + i18n: + zh-CN: 通过数 + en-US: Passes + ja-JP: パス数 + protocol: mongodb + mongodb: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + authenticationDatabase: ^_^authenticationDatabase^_^ + command: serverStatus.metrics.ttl + timeout: ^_^timeout^_^ + + # https://www.mongodb.com/docs/manual/reference/command/hostInfo/ + - name: systemInfo + priority: 4 + i18n: + zh-CN: 系统信息 + en-US: System Info + ja-JP: システム情報 + fields: + - field: currentTime + type: 1 + i18n: + zh-CN: 当前时间 + en-US: Current Time + ja-JP: 現在時刻 + - field: hostname + type: 1 + i18n: + zh-CN: 主机名 + en-US: Host Name + ja-JP: ホスト名 + - field: cpuAddrSize + type: 0 + i18n: + zh-CN: cpu地址大小 + en-US: CPU Address Size + ja-JP: CPUアドレスサイズ + - field: memSizeMB + type: 0 + unit: MB + i18n: + zh-CN: 内存大小(MB) + en-US: Memory Size MB + ja-JP: メモリサイズ(MB) + - field: memLimitMB + type: 0 + unit: MB + i18n: + zh-CN: 内存限制(MB) + en-US: Memory Limit MB + ja-JP: メモリ制限(MB) + - field: numCores + type: 0 + i18n: + zh-CN: CPU 核数 + en-US: Num Cores + ja-JP: コア数 + - field: cpuArch + type: 1 + i18n: + zh-CN: CPU 架构 + en-US: CPU Architecture + ja-JP: CPUアーキテクチャ + - field: numaEnabled + type: 1 + i18n: + zh-CN: numa是否启用 + en-US: Numa Enabled + ja-JP: 有効化 + protocol: mongodb + mongodb: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + authenticationDatabase: ^_^authenticationDatabase^_^ + command: hostInfo.system + timeout: ^_^timeout^_^ + + - name: os + priority: 5 + i18n: + zh-CN: 操作系统信息 + en-US: OS Info + ja-JP: オーエス情報 + fields: + - field: type + type: 1 + i18n: + zh-CN: 操作系统类型 + en-US: Type + ja-JP: タイプ + - field: name + type: 1 + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: version + type: 1 + i18n: + zh-CN: Version + en-US: 版本号 + ja-JP: バージョン + protocol: mongodb + mongodb: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + authenticationDatabase: ^_^authenticationDatabase^_^ + command: hostInfo.os + timeout: ^_^timeout^_^ + + - name: extra + priority: 6 + i18n: + zh-CN: 额外信息 + en-US: Extra Info + ja-JP: 追加情報 + fields: + - field: versionString + type: 1 + i18n: + zh-CN: 版本字符串 + en-US: Version String + ja-JP: バージョン文字列 + - field: libcVersion + type: 1 + i18n: + zh-CN: 标准库版本 + en-US: Libc Version + ja-JP: Libcバージョン + - field: kernelVersion + type: 1 + i18n: + zh-CN: 内核版本 + en-US: Kernel Version + ja-JP: カーネルバージョン + - field: cpuFrequencyMHz + type: 1 + i18n: + zh-CN: CPU 频率(兆赫兹) + en-US: CPU Frequency MHz + ja-JP: CPU周波数(MHz) + - field: cpuFeatures + type: 1 + i18n: + zh-CN: CPU 特性 + en-US: CPU Features + ja-JP: CPU機能 + - field: pageSize + type: 0 + i18n: + zh-CN: 内存页大小 + en-US: Page Size + ja-JP: ページサイズ + - field: numPages + type: 0 + i18n: + zh-CN: 内存页数量 + en-US: Num Pages + ja-JP: ページ数量 + - field: maxOpenFiles + type: 0 + i18n: + zh-CN: 最大打开文件数 + en-US: Max Open Files + ja-JP: 開けるファイルの最大数 + protocol: mongodb + mongodb: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + authenticationDatabase: ^_^authenticationDatabase^_^ + command: hostInfo.extra + timeout: ^_^timeout^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-mongodb_atlas.yml b/hertzbeat-manager/src/main/resources/define/app-mongodb_atlas.yml new file mode 100644 index 0000000..6cee4f7 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-mongodb_atlas.yml @@ -0,0 +1,386 @@ +# 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. + + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: mongodb_atlas +# The monitoring i18n name +name: + zh-CN: MongoDB Atlas 数据库 + en-US: MongoDB Atlas Database + ja-JP: MongoDB Atlas データベース +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 MongoDB Atlas 数据库的通用指标进行测量监控。<br>您可以点击 “<i>新建 MongoDB Atlas 数据库</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors MongoDB Atlas general performance metrics. You could click the "<i>New MongoDB Atlas</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 MongoDB Atlas 數據庫的通用指標進行測量監控。<br>您可以點擊 “<i>新建 MongoDB Atlas 數據庫</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は MongoDB Atlas データベースの一般的なメトリクスを監視します。<br>「<i>新規 MongoDB Atlas データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/mongodb + en-US: https://hertzbeat.apache.org/docs/help/mongodb +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + - field: database + name: + zh-CN: 数据库 + en-US: database + ja-JP: データベース + type: text + # when type is text, use limit to limit string length + limit: 50 + required: true + defaultValue: 'test' + - field: authenticationDatabase + name: + zh-CN: 认证数据库 + en-US: authenticationDatabase + ja-JP: 認証データベース + type: text + limit: 50 + required: true + defaultValue: 'admin' + - field: timeout + # name-param field display i18n name + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 + # field-param field key + - field: model + hide: true + defaultValue: 'mongodb-atlas' + # name-param field display i18n name + name: + zh-CN: 集群模式 + en-US: Cluster Mode + ja-JP: クラスターモード + # type-param field type(most mapping the html input type) + type: text +# can get status information and other config params of MongoDB by running Diagnostic Commands +# https://www.mongodb.com/docs/manual/reference/command/nav-diagnostic/ +# name must be Diagnostic Commands 's name,support . way to get subdocument, should be careful about if the user that need connection have the right to run command + +# collect metrics config list +metrics: + # https://www.mongodb.com/docs/manual/reference/command/buildInfo/#usage + # metrics - buildInfo + - name: buildInfo + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + i18n: + zh-CN: 构建信息 + en-US: Build Info + ja-JP: ビルド情報 + # monitoring metrics list + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + - field: gitVersion + type: 1 + i18n: + zh-CN: git版本 + en-US: Git Version + ja-JP: Gitバージョン + - field: sysInfo + type: 1 + i18n: + zh-CN: 系统信息 + en-US: Sys Info + ja-JP: システム情報 + - field: allocator + type: 1 + i18n: + zh-CN: 分配器 + en-US: Allocator + ja-JP: メモリ割り当て器 + - field: javascriptEngine + type: 1 + i18n: + zh-CN: JavaScript引擎 + en-US: JavaScript Engine + ja-JP: JavaScriptエンジン + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: mongodb + # the config content when protocol is mongodb + mongodb: + #mongodb host: + host: ^_^host^_^ + #mongodb port + port: ^_^port^_^ + #user's username + username: ^_^username^_^ + #user's password + password: ^_^password^_^ + #mongodb's database + database: ^_^database^_^ + #mongodb's model + model: ^_^model^_^ + #Specifies the database in which the User is created + authenticationDatabase: ^_^authenticationDatabase^_^ + #The buildInfo command is an administrative command which returns a build summary for the current mongod. + command: buildInfo + # timeout unit:ms + timeout: ^_^timeout^_^ + + # https://www.mongodb.com/docs/manual/reference/command/serverStatus/#metrics + - name: server_document + priority: 1 + i18n: + zh-CN: 服务器文档 + en-US: Server Document + ja-JP: サーバードキュメント + fields: + - field: delete + type: 0 + i18n: + zh-CN: 已删除数 + en-US: delete + ja-JP: 削除回数 + - field: insert + type: 0 + i18n: + zh-CN: 已插入数 + en-US: insert + ja-JP: インサート回数 + - field: update + type: 0 + i18n: + zh-CN: 更新数 + en-US: update + ja-JP: 更新回数 + - field: query + type: 0 + i18n: + zh-CN: 查询数 + en-US: query + ja-JP: クエリ回数 + - field: getmore + type: 0 + i18n: + zh-CN: 光标中剩余文档的请求数 + en-US: getmore + ja-JP: getmore + - field: command + type: 0 + i18n: + zh-CN: 执行命令操作的总数 + en-US: command + ja-JP: 実行されたコマンド数 + protocol: mongodb + mongodb: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + model: ^_^model^_^ + authenticationDatabase: ^_^authenticationDatabase^_^ + command: serverStatus.opcounters + timeout: ^_^timeout^_^ + + - name: network + priority: 2 + i18n: + zh-CN: 网络操作 + en-US: Network Operation + ja-JP: ネットワーク操作 + fields: + - field: bytesIn + type: 0 + i18n: + zh-CN: 读取字节数 + en-US: Bytes In + ja-JP: 読み取りバイト数 + - field: bytesOut + type: 0 + i18n: + zh-CN: 写入字节数 + en-US: Bytes Out + ja-JP: 書き込みバイト数 + - field: numRequests + type: 0 + i18n: + zh-CN: 请求数 + en-US: Request Num + ja-JP: リクエスト数 + protocol: mongodb + mongodb: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + model: ^_^model^_^ + authenticationDatabase: ^_^authenticationDatabase^_^ + command: serverStatus.network + timeout: ^_^timeout^_^ + + - name: connections + priority: 3 + i18n: + zh-CN: 连接信息 + en-US: Connection Info + ja-JP: 接続情報 + fields: + - field: current + type: 0 + i18n: + zh-CN: 当前正在进行连接数 + en-US: Current Connections + ja-JP: 現在接続数 + - field: available + type: 0 + i18n: + zh-CN: 可用连接数 + en-US: Available Connections + ja-JP: 利用可能な接続数 + - field: totalCreated + type: 0 + i18n: + zh-CN: 总创建连接数 + en-US: Total Created Connections + ja-JP: 作成された接続総数 + protocol: mongodb + mongodb: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + model: ^_^model^_^ + authenticationDatabase: ^_^authenticationDatabase^_^ + command: serverStatus.connections + timeout: ^_^timeout^_^ + + - name: dbstats + priority: 4 + i18n: + zh-CN: 数据库统计 + en-US: Database Statistics + ja-JP: データベース統計 + fields: + - field: db + type: 1 + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: collections + type: 0 + i18n: + zh-CN: 集合数 + en-US: Collections + ja-JP: コレクション数 + - field: views + type: 0 + i18n: + zh-CN: 视图数 + en-US: Views + ja-JP: ビュー数 + - field: objects + type: 0 + i18n: + zh-CN: 文档数 + en-US: Objects + ja-JP: ドキュメント数 + - field: avgObjSize + type: 0 + unit: Bytes + i18n: + zh-CN: 文档平均大小 + en-US: Document Avg Size + ja-JP: ドキュメントの平均サイズ + - field: dataSize + type: 0 + unit: Bytes + i18n: + zh-CN: 文档大小 + en-US: Document Size + ja-JP: ドキュメントサイズ + - field: storageSize + type: 0 + unit: Bytes + i18n: + zh-CN: 使用存储空间大小 + en-US: Storage Size + ja-JP: ストレージサイズ + - field: indexes + type: 0 + i18n: + zh-CN: 索引数 + en-US: Indexes + ja-JP: インデックス数 + - field: indexSize + type: 0 + unit: Bytes + i18n: + zh-CN: 索引大小 + en-US: Index Size + ja-JP: インデックスサイズ + protocol: mongodb + mongodb: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + model: ^_^model^_^ + authenticationDatabase: ^_^authenticationDatabase^_^ + command: dbStats + timeout: ^_^timeout^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-mqtt.yml b/hertzbeat-manager/src/main/resources/define/app-mqtt.yml new file mode 100644 index 0000000..e22bf42 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-mqtt.yml @@ -0,0 +1,226 @@ +# 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. + + +category: service +app: mqtt +name: + zh-CN: MQTT 连接 + en-US: MQTT Connection + ja-JP: MQTT 接続 +help: + zh-CN: HertzBeat 对 MQTT 连接进行监测。<br>您可以点击 “<i>新建 MQTT 连接</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat monitors MQTT connections. <br>You can click "<i>New MQTT connection</i>" and configure it, or select "<i>More actions</i>" to import an existing configuration. + zh-TW: HertzBeat 對 MQTT 連接進行監測。 <br>您可以點選 “<i>新建 MQTT 連線</i>” 並進行配置,或選擇“<i>更多操作</i>”,匯入已有配置。 + ja-JP: Hertzbeat はMQTT 接続を監視します。<br>「<i>新規 MQTT 接続</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/mqtt + en-US: https://hertzbeat.apache.org/docs/help/mqtt +params: + - field: clientId + name: + zh-CN: 客户端ID + en-US: Client Id + ja-JP: クライエントID + type: text + defaultValue: hertzbeat-mqtt-client + required: true + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + required: false + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + - field: host + name: + zh-CN: MQTT的Host + en-US: Target Host + ja-JP: 目標ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + required: true + defaultValue: 1883 + - field: protocol + name: + zh-CN: 连接协议 + en-US: Protocol + ja-JP: 接続プロトコル + type: radio + options: + - label: MQTT + value: MQTT + - label: MQTTS + value: MQTTS + required: true + defaultValue: MQTT + - field: timeout + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + type: number + range: '[0,100000]' + required: true + defaultValue: 10000 + - field: keepalive + name: + zh-CN: 心跳检测时间(s) + en-US: Keep Alive(s) + ja-JP: ハートビート間隔(s) + type: number + range: '[0,100000]' + required: true + defaultValue: 30 + - field: tlsVersion + name: + zh-CN: TLS版本 + en-US: TLS Version + ja-JP: TLSバージョン + type: radio + options: + - label: TLSv1.2 + value: TLSv1.2 + - label: TLSv1.3 + value: TLSv1.3 + defaultValue: TLSv1.2 + required: false + hide: true + - field: insecureSkipVerify + name: + zh-CN: 跳过证书验证 + en-US: Skip Certificate Verification + ja-JP: 証明書の検証をスキップ + type: boolean + defaultValue: false + hide: true + - field: caCert + name: + zh-CN: CA证书 + en-US: CA Certificate + ja-JP: CA証明書 + type: text + required: false + hide: true + - field: enableMutualAuth + name: + zh-CN: 双向认证 + en-US: Enable Mutual Auth + ja-JP: 相互認証 + type: boolean + defaultValue: false + hide: true + - field: clientCert + name: + zh-CN: 客户端证书 + en-US: Client Certificate + ja-JP: クライエントの証明書 + type: text + required: false + hide: true + - field: clientKey + name: + zh-CN: 客户端私钥 + en-US: Client Private Key + ja-JP: クライエントの秘密鍵 + type: text + required: false + hide: true + - field: topic + name: + zh-CN: 主题 + en-US: Topic + ja-JP: トピック + type: text + required: true + - field: testMessage + name: + zh-CN: 测试消息 + en-US: Test message + ja-JP: 試験メッセージ + type: text + required: false + +metrics: + - name: summary + i18n: + zh-CN: 概要 + en-US: Summary + ja-JP: 概要 + priority: 0 + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: canSubscribe + type: 1 + i18n: + zh-CN: 订阅状态 + en-US: Normal subscribe + ja-JP: サブスクライ状態 + - field: canPublish + type: 1 + i18n: + zh-CN: 发布状态 + en-US: Normal publish + ja-JP: メッセージを転送できる + - field: canReceive + type: 1 + i18n: + zh-CN: 接收数据 + en-US: Receive data + ja-JP: 受信されたデータ + - field: canUnSubscribe + type: 1 + i18n: + zh-CN: 取消订阅状态 + en-US: Normal unsubscribe + ja-JP: サブスクライブ解除の状態 + protocol: mqtt + mqtt: + clientId: ^_^clientId^_^ + username: ^_^username^_^ + password: ^_^password^_^ + host: ^_^host^_^ + port: ^_^port^_^ + protocol: ^_^protocol^_^ + timeout: ^_^timeout^_^ + keepalive: ^_^keepalive^_^ + tlsVersion: ^_^tlsVersion^_^ + insecureSkipVerify: ^_^insecureSkipVerify^_^ + caCert: ^_^caCert^_^ + enableMutualAuth: ^_^enableMutualAuth^_^ + clientCert: ^_^clientCert^_^ + clientKey: ^_^clientKey^_^ + topic: ^_^topic^_^ + testMessage: ^_^testMessage^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-mysql.yml b/hertzbeat-manager/src/main/resources/define/app-mysql.yml new file mode 100644 index 0000000..cbe4c95 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-mysql.yml @@ -0,0 +1,1246 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: mysql +# The monitoring i18n name +name: + zh-CN: Mysql数据库 + en-US: Mysql DB + ja-JP: Mysql データベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 Mysql 数据库的通用性能指标(系统信息、性能状态、Innodb、缓存、事物、用户线程、慢SQL等)进行采集监控,支持版本为 Mysql5+。<br>您可以点击“<i>新建 Mysql 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'>JDBC Protocol</a> to collect general performance metrics of MySQL databases (system information, performance status, InnoDB, cache, transactions, user threads, slow SQL, etc.) by configuring SQL. Supported versions include MySQL 5.0 and above. <br>You can click "<i>New MySQL Database</i>" to configure it, or select "<i>More Operations</i>" to import an existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 協議</a> 通過配置 SQL 對 Mysql 數據庫的通用性能指標(系統信息、性能狀態、Innodb、緩存、事物、用戶線程、慢SQL等)進行采集監控,支持版本爲 Mysql5+。<br>您可以點擊“<i>新建 Mysql 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: HertzBeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC プロトコル</a> を介して Mysql データベースの一般的なパフォーマンスのメトリクス(システム情報、パフォーマンスステータス、Innodb、キャッシュ、オブジェクト、ユーザースレッド、遅いSQLなど)収集監視を行う,サポートバージョンは Mysql5+。<br>クリックすることができます“<i>新規 Mysql データベース</i>”および構成,または“<i>その他の操作</i>”,既存の構成のインポートすることができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/mysql + en-US: https://hertzbeat.apache.org/docs/help/mysql +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 3306 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 + # field-param field key + - field: database + # name-param field display i18n name + name: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + # type-param field type(most mapping the html input tag) + type: text + # required-true or false + required: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input tag) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: url + # name-param field display i18n name + name: + zh-CN: URL + en-US: URL + ja-JP: URL + # type-param field type(most mapping the html input tag) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: enableSshTunnel + name: + zh-CN: 是否启用SSH隧道 + en-US: Enable SSH Tunnel + ja-JP: SSHトンネルを有効にするかどうか + type: boolean + required: true + hide: true + - field: sshHost + name: + zh-CN: SSH Host + en-US: SSH Host + ja-JP: SSHホスト + type: text + required: false + placeholder: 'When Enable SSH Tunnel' + hide: true + - field: sshPort + name: + zh-CN: SSH端口 + en-US: SSH Port + ja-JP: SSHポート + type: number + range: '[0,65535]' + required: false + defaultValue: 22 + placeholder: 'When Enable SSH tunnel' + hide: true + - field: sshTimeout + name: + zh-CN: SSH超时时间(ms) + en-US: SSH Timeout(ms) + ja-JP: SSHタイムアウト(ms) + type: number + required: false + range: '[400,200000]' + defaultValue: 6000 + hide: true + - field: sshUsername + name: + zh-CN: SSH用户名 + en-US: SSH Username + ja-JP: SSHユーザー名 + type: text + required: false + placeholder: 'When Enable SSH tunnel' + hide: true + - field: sshPassword + name: + zh-CN: SSH密码 + en-US: SSH Password + ja-JP: SSHパスワード + type: password + required: false + hide: true + - field: sshShareConnection + name: + zh-CN: 是否共享SSH连接 + en-US: Share SSH Connection + ja-JP: SSH接続を共有するかどうか + type: boolean + required: true + defaultValue: true + hide: true + - field: sshPrivateKey + name: + zh-CN: SSH私钥 + en-US: SSH PrivateKey + ja-JP: SSH秘密鍵 + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + required: false + hide: true + - field: sshPrivateKeyPassphrase + name: + zh-CN: SSH密钥短语 + en-US: SSH PrivateKey PassPhrase + ja-JP: SSH秘密鍵フレーズ + type: password + required: false + hide: true + +# collect metrics config list +metrics: + # metrics - basic + - name: basic + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + i18n: + zh-CN: 基础信息 + en-US: Basic Info + ja-JP: 基礎情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + - field: port + type: 1 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: datadir + type: 1 + i18n: + zh-CN: 存储目录 + en-US: Data Directory + ja-JP: データディレクトリ + - field: max_connections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + - field: thread_cache_size + type: 0 + i18n: + zh-CN: 连接池大小 + en-US: Thread Cache Size + ja-JP: 接続プールのサイズ + - field: innodb_buffer_pool_size + type: 0 + unit: KB + i18n: + zh-CN: InnoDB缓冲池的大小 + en-US: Innodb Buffer Pool Size + ja-JP: InnoDBバッファプールのサイズ + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - version + - version_compile_os + - version_compile_machine + - port + - datadir + - max_connections + - thread_cache_size + - table_open_cache + - innodb_buffer_pool_size + # (optional)mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - port=port + - datadir=datadir + - max_connections=max_connections + - version=version+"_"+version_compile_os+"_"+version_compile_machine + # Convert the initial unit of the metric to the final unit + units: + - innodb_buffer_pool_size=B->KB + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # mysql host: ipv4 ipv6 host + host: ^_^host^_^ + # mysql port + port: ^_^port^_^ + # database platform name + platform: mysql + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # database name + database: ^_^database^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + # sql + sql: show global variables where Variable_name like 'version%' or Variable_name = 'max_connections' or Variable_name = 'datadir' or Variable_name = 'port' or Variable_name = 'thread_cache_size' or Variable_name = 'table_open_cache' or Variable_name = 'innodb_buffer_pool_size'; + # JDBC url + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: cache + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + i18n: + zh-CN: 缓存信息 + en-US: Cache Info + ja-JP: キャッシュ情報 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: query_cache_hit_rate + type: 0 + unit: '%' + i18n: + zh-CN: 缓存命中率 + en-US: Query Cache Hit Rate + ja-JP: キャッシュ命中率 + - field: cache_hits + type: 0 + i18n: + zh-CN: 缓存命中数 + en-US: Cache Hits + ja-JP: キャッシュ命中数 + - field: cache_inserts + type: 0 + i18n: + zh-CN: 缓存插入数 + en-US: Cache Inserts + ja-JP: キャッシュ挿入数 + - field: cache_free_blocks + type: 0 + i18n: + zh-CN: 缓存空闲块数量 + en-US: Cache Free Blocks + ja-JP: キャッシュの空きブロック数 + - field: cache_free_memory + type: 0 + unit: KB + i18n: + zh-CN: 空闲缓存大小 + en-US: Cache Free Memory + ja-JP: キャッシュの利用可能メモリ + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - Qcache_hits + - Qcache_inserts + - Qcache_free_blocks + - Qcache_free_memory + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - query_cache_hit_rate= (Qcache_hits + 1) / (Qcache_hits + Qcache_inserts + 1) * 100 + - cache_hits=Qcache_hits + - cache_inserts=Qcache_inserts + - cache_free_blocks=Qcache_free_blocks + - cache_free_memory=Qcache_free_memory + # Convert the initial unit of the metric to the final unit + units: + - cache_free_memory=B->KB + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # jdbc host + host: ^_^host^_^ + # port + port: ^_^port^_^ + # platform name + platform: mysql + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # database name + database: ^_^database^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + # sql + sql: show global status like 'QCache%'; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: performance + priority: 2 + i18n: + zh-CN: 性能信息 + en-US: Performance Info + ja-JP: パフォーマンス情報 + fields: + - field: questions + type: 0 + i18n: + zh-CN: 查询总数 + en-US: Questions + ja-JP: 合計クエリ数 + - field: qps + type: 0 + i18n: + zh-CN: 每秒处理查询数 + en-US: Queries Per Second + ja-JP: クエリーの処理数/秒 + aliasFields: + - uptime + - questions + calculates: + - qps=uptime / questions + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name = 'questions' or Variable_name = 'uptime'; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: innodb + priority: 3 + i18n: + zh-CN: Innodb信息 + en-US: Innodb Info + ja-JP: Innodb情報 + fields: + - field: innodb_data_reads + type: 0 + unit: Times + i18n: + zh-CN: 磁盘读取次数 + en-US: Innodb Data Reads + ja-JP: ディスク読み取り回数 + - field: innodb_data_writes + type: 0 + unit: Times + i18n: + zh-CN: 磁盘写入次数 + en-US: Innodb Data Writes + ja-JP: ディスクへの書き込み回数 + - field: innodb_data_read + type: 0 + unit: KB + i18n: + zh-CN: 磁盘读取数据量 + en-US: Innodb Data Read + ja-JP: Innodb読み取りデータ量(KB) + - field: innodb_data_written + type: 0 + unit: KB + i18n: + zh-CN: 磁盘写入数据量 + en-US: Innodb Data Written + ja-JP: Innodb書き込みデータ量(KB) + - field: innodb_buffer_hit_rate + type: 0 + unit: '%' + i18n: + zh-CN: Innodb 缓存命中数 + en-US: Innodb Buffer Hit Rate + ja-JP: Innodbバッファーキャッシュ命中率 + aliasFields: + - Innodb_data_reads + - Innodb_data_writes + - Innodb_data_read + - Innodb_data_written + - Innodb_buffer_pool_read_requests + - Innodb_buffer_pool_read_ahead + - Innodb_buffer_pool_reads + calculates: + - innodb_buffer_hit_rate= (Innodb_buffer_pool_read_requests + 1) / (Innodb_buffer_pool_read_requests + Innodb_buffer_pool_read_ahead + Innodb_buffer_pool_reads + 1) * 100 + - innodb_data_reads=Innodb_data_reads + - innodb_data_writes=Innodb_data_writes + - innodb_data_read=Innodb_data_read + - innodb_data_written=Innodb_data_written + units: + - innodb_data_read=B->KB + - innodb_data_written=B->KB + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name like 'innodb%'; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: status + priority: 4 + i18n: + zh-CN: 状态信息 + en-US: Status Info + ja-JP: 状態情報 + fields: + - field: uptime + type: 0 + unit: s + i18n: + zh-CN: 运行时间 + en-US: Uptime + ja-JP: 実行時間 + - field: com_select + type: 0 + i18n: + zh-CN: 查询次数 + en-US: Com Select + ja-JP: セレクト回数 + - field: com_insert + type: 0 + i18n: + zh-CN: 插入次数 + en-US: Com Insert + ja-JP: 挿入回数 + - field: com_update + type: 0 + i18n: + zh-CN: 更新次数 + en-US: Com Update + ja-JP: 更新回数 + - field: com_delete + type: 0 + i18n: + zh-CN: 删除次数 + en-US: Com Delete + ja-JP: 削除回数 + - field: com_commit + type: 0 + i18n: + zh-CN: 事务提交次数 + en-US: Com Commit + ja-JP: トランザクションのコミット回数 + - field: com_rollback + type: 0 + i18n: + zh-CN: 事务回滚次数 + en-US: Com Rollback + ja-JP: トランザクションのロールバック回数 + - field: threads_created + type: 0 + i18n: + zh-CN: 线程创建数 + en-US: Threads Created + ja-JP: スレッド作成数 + - field: threads_connected + type: 0 + i18n: + zh-CN: 连接线程数 + en-US: Threads Connected + ja-JP: 接続スレッド数 + - field: threads_cached + type: 0 + i18n: + zh-CN: 缓存线程数 + en-US: Threads Cached + ja-JP: キャッシュされたスレッド数 + - field: threads_running + type: 0 + i18n: + zh-CN: 活动线程数 + en-US: Threads Running + ja-JP: アクティブなスレッド数 + - field: qps + type: 0 + i18n: + zh-CN: 每秒处理查询数 + en-US: Com Insert + ja-JP: クエリーの処理数/秒 + aliasFields: + - uptime + - com_select + - com_insert + - com_update + - com_delete + - com_commit + - com_rollback + - threads_created + - threads_connected + - threads_cached + - threads_running + - questions + calculates: + - qps=uptime / questions + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name like 'thread%' or Variable_name = 'com_select' or Variable_name = 'com_insert' or Variable_name = 'com_update' or Variable_name = 'com_delete' or Variable_name = 'com_commit' or Variable_name = 'com_rollback' or Variable_name = 'questions' or Variable_name = 'uptime'; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: handler + priority: 5 + i18n: + zh-CN: 处理器信息 + en-US: Handler Info + ja-JP: ハンドラ情報 + fields: + - field: handler_commit + type: 0 + i18n: + zh-CN: 处理的事务提交数 + en-US: Handler Commit + ja-JP: ハンドラのコミット操作数 + - field: handler_delete + type: 0 + i18n: + zh-CN: 处理的删除操作数 + en-US: Handler Delete + ja-JP: ハンドラの削除操作数 + - field: handler_write + type: 0 + i18n: + zh-CN: 处理的写入操作数 + en-US: Handler Write + ja-JP: ハンドラの書き込み操作数 + - field: handler_update + type: 0 + i18n: + zh-CN: 处理的更新操作数 + en-US: Handler Update + ja-JP: ハンドラの更新操作数 + - field: handler_discover + type: 0 + i18n: + zh-CN: 处理的发现操作数 + en-US: Handler Discover + ja-JP: ハンドラのディスカバー操作数 + - field: handler_prepare + type: 0 + i18n: + zh-CN: 处理的准备操作数 + en-US: Handler Prepare + ja-JP: ハンドラの準備操作数 + - field: handler_read_first + type: 0 + i18n: + zh-CN: 读取首行操作数 + en-US: Handler Read First + ja-JP: ハンドラの最初の行を読み取り操作数 + - field: handler_read_rnd + type: 0 + i18n: + zh-CN: 随机读取操作数 + en-US: Handler Read RND + ja-JP: ハンドラのランダム読み取り操作数 + - field: handler_read_next + type: 0 + i18n: + zh-CN: 读取下一行操作数 + en-US: Handler Read Next + ja-JP: ハンドラの次の行を読み取る操作数 + - field: handler_read_rnd_next + type: 0 + i18n: + zh-CN: 随机读取下一行操作数 + en-US: Handler Read RND Next + ja-JP: ハンドラの次の行をランダムに読み取る操作数 + - field: handler_read_key + type: 0 + i18n: + zh-CN: 按索引键读取操作数 + en-US: Handler Read Key + ja-JP: ハンドラのインデックスを使用して読み取る操作数 + - field: handler_external_lock + type: 0 + i18n: + zh-CN: 外部锁定操作数 + en-US: Handler External Key + ja-JP: ハンドラの外部のキーを使用して読み取る操作数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status like 'Handler%'; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: connection + priority: 6 + i18n: + zh-CN: 连接信息 + en-US: Connection Info + ja-JP: 接続情報 + fields: + - field: connections + type: 0 + i18n: + zh-CN: 连接的总次数 + en-US: Connections + ja-JP: 接続の合計回数 + - field: max_used_connections + type: 0 + i18n: + zh-CN: 历史最大连接数 + en-US: Max Used Connections + ja-JP: 過去最大接続数 + - field: aborted_connects + type: 0 + i18n: + zh-CN: 失败连接数 + en-US: Aborted Connects + ja-JP: 失敗した接続数 + - field: aborted_clients + type: 0 + i18n: + zh-CN: 客户端中断数 + en-US: Aborted Clients + ja-JP: クライエントの割り込み数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: thread + priority: 7 + i18n: + zh-CN: 线程信息 + en-US: Thread Info + ja-JP: スレッド情報 + fields: + - field: threads_created + type: 0 + i18n: + zh-CN: 创建线程数 + en-US: Threads Created + ja-JP: スレッド作成数 + - field: threads_connected + type: 0 + i18n: + zh-CN: 连接线程数 + en-US: Threads Connected + ja-JP: 接続スレッド数 + - field: threads_cached + type: 0 + i18n: + zh-CN: 缓存线程数 + en-US: CThreads Cached + ja-JP: キャッシュスレッド数 + - field: threads_running + type: 0 + i18n: + zh-CN: 运行线程数 + en-US: Threads Running + ja-JP: 活動中のスレッド数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status like 'thread%'; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: tmp + priority: 8 + i18n: + zh-CN: 临时表信息 + en-US: TMP Info + ja-JP: 一時テーブル情報 + fields: + - field: created_tmp_tables + type: 0 + i18n: + zh-CN: 创建的临时表数量 + en-US: Created TMP Tables + ja-JP: 一時テーブルの作成数 + - field: created_tmp_disk_tables + type: 0 + i18n: + zh-CN: 创建的临时磁盘表数量 + en-US: Created TMP Disk Tables + ja-JP: 一時ディスクテーブルの作成数 + - field: created_tmp_files + type: 0 + i18n: + zh-CN: 创建的临时文件数量 + en-US: Created TMP Files + ja-JP: 一時ファイルの作成数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name like '%tmp%'; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: select_type + priority: 9 + i18n: + zh-CN: 查询类型信息 + en-US: Select Type Info + ja-JP: 検索タイプ情報 + fields: + - field: select_full_join + type: 0 + i18n: + zh-CN: 全表连接数 + en-US: Select Full Join + ja-JP: 全件結合数 + - field: select_full_range_join + type: 0 + i18n: + zh-CN: 带范围的全表连接数 + en-US: Select Full Range Join + ja-JP: 範囲検索付きの全件結合数 + - field: select_range + type: 0 + i18n: + zh-CN: 索引范围查询数 + en-US: Select Range + ja-JP: 範囲検索数 + - field: select_range_check + type: 0 + i18n: + zh-CN: 索引范围内二次检查数 + en-US: Select Range Check + ja-JP: 範囲内の二次検索数 + - field: select_scan + type: 0 + i18n: + zh-CN: 全表扫描查询数 + en-US: Select Scan + ja-JP: テーブル全体をスキャンする回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name like 'select%'; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: sort + priority: 10 + i18n: + zh-CN: 排序信息 + en-US: Sort Info + ja-JP: ソート情報 + fields: + - field: sort_rows + type: 0 + i18n: + zh-CN: 排序总行数 + en-US: Sort Rows + ja-JP: ソートの合計行数 + - field: sort_range + type: 0 + i18n: + zh-CN: 范围条件排序次数 + en-US: Sort Range + ja-JP: 範囲付きのソート回数 + - field: sort_merge_passes + type: 0 + i18n: + zh-CN: 合并排序段次数 + en-US: Sort Merge Passes + ja-JP: マージソート回数 + - field: sort_scan + type: 0 + i18n: + zh-CN: 全表扫描排序次数 + en-US: Sort Scan + ja-JP: テーブル全体をスキャンしてソートする回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name like 'sort%'; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: table_lock + priority: 11 + i18n: + zh-CN: 表级锁信息 + en-US: Table Lock Info + ja-JP: テーブルロック情報 + fields: + - field: table_locks_immediate + type: 0 + i18n: + zh-CN: 立即获得表锁次数 + en-US: Table Locks Immediate + ja-JP: テーブルロックの即時取得回数 + - field: table_locks_waited + type: 0 + i18n: + zh-CN: 须等待获取表锁次数 + en-US: Table Locks Waited + ja-JP: テーブルロック取得待ち回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: show global status where Variable_name like 'table_lock%'; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: process_state + priority: 12 + i18n: + zh-CN: 进程状态信息 + en-US: Process State Info + ja-JP: プロセス状態情報 + fields: + - field: state + type: 1 + label: true + i18n: + zh-CN: 进程状态 + en-US: State + ja-JP: プロセス状態 + - field: num + type: 0 + i18n: + zh-CN: 该状态进程数量 + en-US: Num + ja-JP: プロセス総数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: multiRow + sql: select state, count(*) as num from information_schema.PROCESSLIST where state != '' group by state; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: slow_sql + priority: 13 + i18n: + zh-CN: 慢查询信息 + en-US: Slow Sql Info + ja-JP: スローSQL情報 + fields: + - field: id + type: 1 + label: true + i18n: + zh-CN: ID + en-US: ID + ja-JP: ID + - field: sql_text + type: 1 + i18n: + zh-CN: SQL 文本 + en-US: SQL Text + ja-JP: SQL文のテキスト + - field: start_time + type: 1 + i18n: + zh-CN: 开始时间 + en-US: Start Time + ja-JP: 開始時間 + - field: db + type: 1 + i18n: + zh-CN: 数据库 + en-US: Database + ja-JP: データベース + - field: query_time + type: 3 + i18n: + zh-CN: 查询时间 + en-US: Query Time + ja-JP: クエリー時間 + aliasFields: + - sql_text + - start_time + - db + - query_time + calculates: + - id= start_time + sql_text + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: multiRow + sql: select sql_text, start_time, db, query_time from mysql.slow_log; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + - name: account_expiry + priority: 100 + i18n: + zh-CN: 数据库账号有效期 + en-US: Account Expiry + ja-JP: アカウント有効期限 + fields: + - field: user + type: 1 + label: true + i18n: + zh-CN: 用户 + en-US: User + ja-JP: ユーザー + - field: host + type: 1 + label: true + i18n: + zh-CN: 主机 + en-US: Host + ja-JP: ホスト + - field: days_left + type: 0 + i18n: + zh-CN: 剩余天数 + en-US: Days Left + ja-JP: 残り日数 + - field: password_expired + type: 1 + i18n: + zh-CN: 是否已过期 + en-US: Password Expired + ja-JP: 期限切れ + + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: mysql + timeout: ^_^timeout^_^ + queryType: multiRow + sql: | + SELECT + user, + host, + IF(password_lifetime IS NULL, + NULL, + password_lifetime - DATEDIFF(NOW(), password_last_changed) + ) AS days_left, + password_expired + FROM mysql.user; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + diff --git a/hertzbeat-manager/src/main/resources/define/app-nacos.yml b/hertzbeat-manager/src/main/resources/define/app-nacos.yml new file mode 100644 index 0000000..6b2e674 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-nacos.yml @@ -0,0 +1,179 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring mid-middleware +category: mid +# The monitoring: nacos +app: nacos +# The monitoring i18n name +name: + zh-CN: Nacos分布式 + en-US: Nacos Server + ja-JP: Nacosサーバー +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 通过调用 <a href="https://nacos.io/zh-cn/docs/monitor-guide.html">Nacos Metrics 接口</a> 对 Nacos 注册配置中心服务的通用指标进行采集监控。<br>注意⚠️:需要您提前 <a href="https://nacos.io/zh-cn/docs/monitor-guide.html">开启暴露 Nacos Metrics 数据</a>,支持 Nacos 版本0.8+,您可以点击 “<i>新建 Nacos</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors metrics of the Nacos Server by calling the <a href="https://nacos.io/en-us/docs/monitor-guide.html">Nacos Metrics Api</a>. <br>Note⚠️:You need to <a href="https://nacos.io/en-us/docs/monitor-guide.html">enable exposure of Nacos Metrics data</a> in advance, support Nacos version 0.8+ , you can click "<i>New Nacos</i>" and configure it, or select "<i>More Action</i>" to import the existing configuration. + zh-TW: Hertzbeat 通過調用 <a href="https://nacos.io/zh-cn/docs/monitor-guide.html">Nacos Metrics 接口</a> 對 Nacos 注冊配置中心服務的通用指標進行采集監控。<br>注意⚠️:需要您提前 <a href="https://nacos.io/zh-cn/docs/monitor-guide.html">開啓暴露 Nacos Metrics 數據</a>,支持 Nacos 版本0.8+,您可以點擊 “<i>新建 Nacos</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeatは、<a href="https://nacos.io/zh-cn/docs/monitor-guide.html">Nacos Metricsインタフェース</a>は、Nacosレジストレーション構成センターサービスの共通指標を収集監視します。<br/>注意⚠️:事前に<a href="https://nacos.io/zh-cn/docs/monitor-guide.html">Nacos Metricsデータの暴露をオンにし</a>、Nacosバージョン0.8+をサポートします。「<i>新規Nacos</i>」をクリックして構成するか、「<i>その他の操作</i>」を選択して既存の構成をインポートできます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/nacos + en-US: https://hertzbeat.apache.org/docs/help/nacos +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 服务器Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # required-true or false + - field: port + # name-param field display i18n name + name: + zh-CN: Nacos服务端口 + en-US: Nacos Port + ja-JP: Nacosポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # required-true or false + defaultValue: 8848 + +# collect metrics config list +metrics: + - name: system_cpu_usage + i18n: + zh-CN: CPU 使用率 + en-US: CPU Usage + ja-JP: CPU 使用率 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + fields: + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + aliasFields: + - value + calculates: + - usage=value + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /nacos/actuator/prometheus + # http method: GET POST PUT DELETE PATCH + method: GET + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: prometheus + + - name: jvm_memory_used_bytes + i18n: + zh-CN: JVM 内存使用 + en-US: JVM Memory Used + ja-JP: Java仮想マシンのメモリ使用量 + priority: 1 + fields: + - field: area + type: 1 + i18n: + zh-CN: 区域 + en-US: Area + ja-JP: エリア + - field: id + type: 1 + label: true + i18n: + zh-CN: ID + en-US: ID + ja-JP: ID + - field: value + type: 0 + unit: MB + i18n: + zh-CN: 大小 + en-US: Size + ja-JP: サイズ + units: + - value=B->MB + # The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # The config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: /nacos/actuator/prometheus + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: nacos_monitor + i18n: + zh-CN: Nacos 详情 + en-US: Nacos Monitor + ja-JP: Nacos 詳細情報 + priority: 2 + fields: + - field: module + type: 1 + i18n: + zh-CN: 模块 + en-US: Module + ja-JP: モジュール + - field: name + type: 1 + i18n: + zh-CN: 指标名称 + en-US: Metric Name + ja-JP: メトリクス名 + - field: value + type: 0 + i18n: + zh-CN: 大小 + en-US: Value + ja-JP: サイズ + # The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # The config content when protocol is http + http: + # Host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: /nacos/actuator/prometheus + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus diff --git a/hertzbeat-manager/src/main/resources/define/app-nacos_sd.yml b/hertzbeat-manager/src/main/resources/define/app-nacos_sd.yml new file mode 100644 index 0000000..e79a4e8 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-nacos_sd.yml @@ -0,0 +1,115 @@ +# 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. + +# The monitoring type category: service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: __system__ +# Monitoring application type name (consistent with file name) eg: linux windows tomcat mysql aws... +app: nacos_sd +# The monitoring i18n name +name: + zh-CN: Nacos 服务发现 + en-US: Nacos Service Discovery + ja-JP: Nacos サービスディスカバリー +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: __nacos_sd_host__ + # name-param field display i18n name + name: + zh-CN: Nacos 服务发现 Host + en-US: Nacos Service Discovery Host + ja-JP: ホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true + - field: __nacos_sd_port__ + name: + zh-CN: Nacos 服务发现 Port + en-US: Nacos Service Discovery Port + ja-JP: ポート + type: number + required: true + - field: __nacos_sd_username__ + name: + zh-CN: Nacos 服务发现 Username + en-US: Nacos Service Discovery User Name + ja-JP: ユーザー名 + type: text + required: false + - field: __nacos_sd_password__ + name: + zh-CN: Nacos 服务发现 Password + en-US: Nacos Service Discovery Password + ja-JP: パスワード + type: password + required: false + - field: __nacos_sd_namespace__ + name: + zh-CN: Nacos 服务发现 Namespace + en-US: Nacos Service Discovery Namespace + ja-JP: 名前空間 + type: text + required: false + - field: __nacos_sd_groupName__ + name: + zh-CN: Nacos 服务发现 Group Name + en-US: Nacos Service Discovery Group Name + ja-JP: グループ名 + type: text + required: false + - field: __nacos_sd_serviceName__ + name: + zh-CN: Nacos 服务发现 Service Name + en-US: Nacos Service Discovery Service Name + ja-JP: サービス名 + type: text + required: false + +metrics: + - name: target + i18n: + zh-CN: 监控目标 + en-US: Monitor Target + ja-JP: 監視対象 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: host + type: 1 + i18n: + zh-CN: Host + en-US: Host + ja-JP: ホスト + - field: port + type: 1 + i18n: + zh-CN: Port + en-US: Port + ja-JP: ポート + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: nacos_sd + # the config content when protocol is nacos_sd + nacos_sd: + host: ^_^__nacos_sd_host__^_^ + port: ^_^__nacos_sd_port__^_^ + namespace: ^_^__nacos_sd_namespace__^_^ + username: ^_^__nacos_sd_username__^_^ + password: ^_^__nacos_sd_password__^_^ + groupName: ^_^__nacos_sd_groupName__^_^ + serviceName: ^_^__nacos_sd_serviceName__^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-nebula_graph.yml b/hertzbeat-manager/src/main/resources/define/app-nebula_graph.yml new file mode 100644 index 0000000..72dae13 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-nebula_graph.yml @@ -0,0 +1,802 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring mid-middleware +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: nebula_graph +# The app api i18n name +name: + zh-CN: NebulaGraph + en-US: NebulaGraph + ja-JP: NebulaGraph +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 将调用NebulaGraph提供的 HTTP API 监控接口,对nebulaGraph及存储数据的rocksdb的指标进行监测。 + en-US: Hertzbeat will call the HTTP API monitoring interface provided by NebulaGraph to monitor the indicators of NebulaGraph and the Rocksdb that stores data. + zh-TW: Hertzbeat將調用NebulaGraph提供的HTTP API監控介面,對nebulaGraph及存儲數據的rocksdb的名額進行監測。 + ja-JP: Hertzbeatは、NebulaGraphが提供されたHTTP APIを呼び出してnebulaGraphとrocksdbのメトリクスを監視します。 +# The description and help of this monitoring type +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/nebulaGraph + en-US: https://hertzbeat.apache.org/docs/help/nebulaGraph +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: graphPort + # name-param field display i18n name + name: + zh-CN: graph端口 + en-US: graphPort + ja-JP: graphポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 19669 + - field: timePeriod + # name-param field display i18n name + name: + zh-CN: graph指标统计的时间范围(s) + en-US: graph indicator statistics time range (s) + ja-JP: graph統計情報の時間範囲(s) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,3600]' + # required-true or false + required: true + # default value 6000 + defaultValue: 60 + - field: storagePort + # name-param field display i18n name + name: + zh-CN: storage端口 + en-US: storagePort + ja-JP: ストレージポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 19779 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 + + +# collect metrics config list +metrics: + # metrics - available + - name: nebulaGraph_stats + i18n: + zh-CN: nebulaGraph 统计信息 + en-US: nebulaGraph stats + ja-JP: nebulaGraph統計情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: num_queries_hit_memory_watermark_rate + type: 0 + i18n: + zh-CN: 达到内存水位线的语句的数量(rate) + en-US: num_queries_hit_memory_watermark.rate + ja-JP: メモリーウォーターマークに達したクエリ数(rate) + - field: num_queries_hit_memory_watermark_sum + type: 0 + i18n: + zh-CN: 达到内存水位线的语句的数量(sum) + en-US: num_queries_hit_memory_watermark.sum + ja-JP: メモリーウォーターマークに達したクエリ数(sum) + - field: num_reclaimed_expired_sessions_rate + type: 0 + i18n: + zh-CN: 服务端主动回收的过期的会话数量(rate) + en-US: num_reclaimed_expired_sessions.rate + ja-JP: 期限切れセッションのリサイクル数(rate) + - field: num_reclaimed_expired_sessions_sum + type: 0 + i18n: + zh-CN: 服务端主动回收的过期的会话数量(sum) + en-US: num_reclaimed_expired_sessions.sum + ja-JP: 期限切れセッションのリサイクル数(sum) + - field: slow_query_latency_us_avg + type: 0 + i18n: + zh-CN: 慢查询延迟时间(avg) + en-US: slow_query_latency_us.avg + ja-JP: スロークエリーの平均レイテンシー + - field: slow_query_latency_us_p75 + type: 0 + i18n: + zh-CN: 慢查询延迟时间(p75) + en-US: slow_query_latency_us.p75 + ja-JP: スロークエリーのレイテンシー(p75) + - field: slow_query_latency_us_p95 + type: 0 + i18n: + zh-CN: 慢查询延迟时间(p95) + en-US: slow_query_latency_us.p95 + ja-JP: スロークエリーのレイテンシー(p95) + - field: slow_query_latency_us_p99 + type: 0 + i18n: + zh-CN: 慢查询延迟时间(p99) + en-US: slow_query_latency_us.p99 + ja-JP: スロークエリーのレイテンシー(p99) + - field: slow_query_latency_us_p999 + type: 0 + i18n: + zh-CN: 慢查询延迟时间(p999) + en-US: slow_query_latency_us.p999 + ja-JP: スロークエリーのレイテンシー(p999) + - field: query_latency_us_avg + type: 0 + i18n: + zh-CN: 查询延迟时间(avg) + en-US: query_latency_us.avg + ja-JP: クエリーの平均レイテンシー + - field: query_latency_us_p75 + type: 0 + i18n: + zh-CN: 查询延迟时间(p75) + en-US: query_latency_us.p75 + ja-JP: クエリーのレイテンシー(p75) + - field: query_latency_us_p95 + type: 0 + i18n: + zh-CN: 查询延迟时间(p95) + en-US: query_latency_us.p95 + ja-JP: クエリーのレイテンシー(p95) + - field: query_latency_us_p99 + type: 0 + i18n: + zh-CN: 查询延迟时间(p99) + en-US: query_latency_us.p99 + ja-JP: クエリーのレイテンシー(p99) + - field: query_latency_us_p999 + type: 0 + i18n: + zh-CN: 查询延迟时间(p999) + en-US: query_latency_us_p999 + ja-JP: クエリーのレイテンシー(p999) + - field: num_auth_failed_sessions_bad_username_password_rate + type: 0 + i18n: + zh-CN: 因用户名密码错误导验证失败的会话数量(rate) + en-US: num_auth_failed_sessions_bad_username_password.rate + ja-JP: ユーザー名とパスワードのエラーにより認証に失敗したセッション数(rate) + - field: num_auth_failed_sessions_bad_username_password_sum + type: 0 + i18n: + zh-CN: 因用户名密码错误导验证失败的会话数量(sum) + en-US: num_auth_failed_sessions_bad_username_password.sum + ja-JP: ユーザー名とパスワードのエラーにより認証に失敗したセッション数(sum) + - field: num_queries_rate + type: 0 + i18n: + zh-CN: 查询次数(rate) + en-US: num_queries.rate + ja-JP: クエリ回数(rate) + - field: num_queries_sum + type: 0 + i18n: + zh-CN: 查询次数(sum) + en-US: num_queries.sum + ja-JP: クエリ回数(sum) + - field: num_sort_executors_rate + type: 0 + i18n: + zh-CN: 排序(Sort)算子执行时间(rate) + en-US: num_sort_executors.rate + ja-JP: ソート実行時間(rate) + - field: num_sort_executors_sum + type: 0 + i18n: + zh-CN: 排序(Sort)算子执行时间(sum) + en-US: num_sort_executors.sum + ja-JP: ソート実行時間(sum) + - field: num_rpc_sent_to_storaged_failed_rate + type: 0 + i18n: + zh-CN: Graphd 服务发给 Storaged 服务的 RPC 请求失败的数量(rate) + en-US: num_rpc_sent_to_storaged_failed.rate + ja-JP: GraphdサービスからStoragedサービスへのRPCリクエスト失敗数(rate) + - field: num_rpc_sent_to_storaged_failed_sum + type: 0 + i18n: + zh-CN: Graphd 服务发给 Storaged 服务的 RPC 请求失败的数量(sum) + en-US: num_rpc_sent_to_storaged_failed.sum + ja-JP: GraphdサービスからStoragedサービスへのRPCリクエスト失敗数(sum) + - field: num_auth_failed_sessions_rate + type: 0 + i18n: + zh-CN: 登录验证失败的会话数量(rate) + en-US: num_auth_failed_sessions.rate + ja-JP: 権限認証に失敗したセッション数(rate) + - field: num_auth_failed_sessions_sum + type: 0 + i18n: + zh-CN: 登录验证失败的会话数量(sum) + en-US: num_auth_failed_sessions.sum + ja-JP: 権限認証に失敗したセッション数(sum) + - field: num_query_errors_rate + type: 0 + i18n: + zh-CN: 查询报错语句数量(rate) + en-US: num_query_errors_rate + ja-JP: クエリエラー数(rate) + - field: num_query_errors_sum + type: 0 + i18n: + zh-CN: 查询报错语句数量(sum) + en-US: num_query_errors.sum + ja-JP: クエリエラー数(sum) + - field: num_killed_queries_rate + type: 0 + i18n: + zh-CN: 被终止的查询数量(rate) + en-US: num_killed_queries.rate + ja-JP: 終了したクエリ数(rate) + - field: num_killed_queries_sum + type: 0 + i18n: + zh-CN: 被终止的查询数量(sum) + en-US: num_killed_queries.sum + ja-JP: 終了したクエリ数(sum) + - field: num_query_errors_leader_changes_rate + type: 0 + i18n: + zh-CN: 因查询错误而导致的 Leader 变更的次数(rate) + en-US: num_query_errors_leader_changes.rate + ja-JP: クエリーエラーによるリーダーの変更数(rate) + - field: num_query_errors_leader_changes_sum + type: 0 + i18n: + zh-CN: 因查询错误而导致的 Leader 变更的次数(sum) + en-US: num_query_errors_leader_changes.sum + ja-JP: クエリーエラーによるリーダーの変更数(sum) + - field: num_rpc_sent_to_metad_rate + type: 0 + i18n: + zh-CN: Graphd 服务发给 Metad 服务的 RPC 请求数量(rate) + en-US: num_rpc_sent_to_metad.rate + ja-JP: GraphdサービスがMetadサービスへのRPCリクエスト数(rate) + - field: num_rpc_sent_to_metad_sum + type: 0 + i18n: + zh-CN: Graphd 服务发给 Metad 服务的 RPC 请求数量(sum) + en-US: num_rpc_sent_to_metad.sum + ja-JP: GraphdサービスがMetadサービスへのRPCリクエスト数(sum) + - field: num_slow_queries_rate + type: 0 + i18n: + zh-CN: 慢查询次数(rate) + en-US: num_slow_queries.rate + ja-JP: スロークエリ回数(rate) + - field: num_slow_queries_sum + type: 0 + i18n: + zh-CN: 慢查询次数(sum) + en-US: num_slow_queries.sum + ja-JP: スロークエリ回数(sum) + - field: num_active_sessions_sum + type: 0 + i18n: + zh-CN: 活跃的会话数的变化数(rate) + en-US: num_active_sessions.rate + ja-JP: アクティブなセッション数(rate) + - field: num_active_queries_sum + type: 0 + i18n: + zh-CN: 活跃的查询语句数的变化数(sum) + en-US: num_active_queries.sum + ja-JP: アクティブなセッション数(sum) + - field: num_sentences_rate + type: 0 + i18n: + zh-CN: Graphd 服务接收的语句数(rate) + en-US: num_sentences.rate + ja-JP: 文数(rate) + - field: num_sentences_sum + type: 0 + i18n: + zh-CN: Graphd 服务接收的语句数(sum) + en-US: num_sentences.sum + ja-JP: 文数(sum) + - field: num_aggregate_executors_rate + type: 0 + i18n: + zh-CN: 聚合(Aggregate)算子执行时间(rate) + en-US: num_aggregate_executors.rate + ja-JP: アグリゲート実行時間(rate) + - field: num_aggregate_executors_sum + type: 0 + i18n: + zh-CN: 聚合(Aggregate)算子执行时间(sum) + en-US: num_aggregate_executors.sum + ja-JP: アグリゲート実行時間(sum) + - field: optimizer_latency_us_avg + type: 0 + i18n: + zh-CN: 优化器阶段延迟时间(avg) + en-US: optimizer_latency_us.avg + ja-JP: オプティマイザの平均レイテンシー + - field: optimizer_latency_us_p75 + type: 0 + i18n: + zh-CN: 优化器阶段延迟时间(p75) + en-US: optimizer_latency_us.p75 + ja-JP: オプティマイザのレイテンシー(p75) + - field: optimizer_latency_us_p95 + type: 0 + i18n: + zh-CN: 优化器阶段延迟时间(p95) + en-US: optimizer_latency_us.p95 + ja-JP: オプティマイザのレイテンシー(p95) + - field: optimizer_latency_us_p99 + type: 0 + i18n: + zh-CN: 优化器阶段延迟时间(p99) + en-US: optimizer_latency_us.p99 + ja-JP: オプティマイザのレイテンシー(p99) + - field: optimizer_latency_us_p999 + type: 0 + i18n: + zh-CN: 优化器阶段延迟时间(p999) + en-US: optimizer_latency_us.p999 + ja-JP: オプティマイザのレイテンシー(p999) + - field: num_rpc_sent_to_metad_failed_rate + type: 0 + i18n: + zh-CN: Graphd 服务发给 Metad 的 RPC 请求失败的数量(rate) + en-US: num_rpc_sent_to_metad_failed.rate + ja-JP: GraphdサービスがMetadサービスへのRPCリクエスト失敗数(rate) + - field: num_rpc_sent_to_metad_failed_sum + type: 0 + i18n: + zh-CN: Graphd 服务发给 Metad 的 RPC 请求失败的数量(sum) + en-US: num_rpc_sent_to_metad_failed.sum + ja-JP: GraphdサービスがMetadサービスへのRPCリクエスト失敗数(sum) + - field: num_indexscan_executors_rate + type: 0 + i18n: + zh-CN: 索引扫描(IndexScan)算子执行时间(rate) + en-US: num_indexscan_executors.rate + ja-JP: 索引スキャン実行時間(rate) + - field: num_indexscan_executors_sum + type: 0 + i18n: + zh-CN: 索引扫描(IndexScan)算子执行时间(sum) + en-US: num_indexscan_executors.sum + ja-JP: 索引スキャン実行時間(sum) + - field: num_opened_sessions_rate + type: 0 + i18n: + zh-CN: 服务端建立过的会话数量(rate) + en-US: num_opened_sessions.rate + ja-JP: 開かれたセッション数(rate) + - field: num_opened_sessions_sum + type: 0 + i18n: + zh-CN: 服务端建立过的会话数量(sum) + en-US: num_opened_sessions.sum + ja-JP: 開かれたセッション数(sum) + - field: num_auth_failed_sessions_out_of_max_allowed_rate + type: 0 + i18n: + zh-CN: 因为超过FLAG_OUT_OF_MAX_ALLOWED_CONNECTIONS参数导致的验证登录失败的会话数量(rate) + en-US: num_auth_failed_sessions_out_of_max_allowed.rate + ja-JP: FLAG_OUT_OF_MAX_ALLOWED_CONNECTIONSが超過したため、認証ログインに失敗したセッション数(rate) + - field: num_auth_failed_sessions_out_of_max_allowed_sum + type: 0 + i18n: + zh-CN: 因为超过FLAG_OUT_OF_MAX_ALLOWED_CONNECTIONS参数导致的验证登录失败的会话数量(sum) + en-US: num_auth_failed_sessions_out_of_max_allowed.sum + ja-JP: FLAG_OUT_OF_MAX_ALLOWED_CONNECTIONSが超過したため、認証ログインに失敗したセッション数(sum) + - field: num_rpc_sent_to_storaged_rate + type: 0 + i18n: + zh-CN: Graphd 服务发给 Storaged 服务的 RPC 请求数量(rate) + en-US: num_rpc_sent_to_storaged.rate + ja-JP: GraphdサービスがStoragedサービスへのRPCリクエスト数(rate) + - field: num_rpc_sent_to_storaged_sum + type: 0 + i18n: + zh-CN: Graphd 服务发给 Storaged 服务的 RPC 请求数量(sum) + en-US: num_rpc_sent_to_storaged.sum + ja-JP: GraphdサービスがStoragedサービスへのRPCリクエスト数(sum) + aliasFields: + - responseTime + - num_queries_hit_memory_watermark.rate + - num_queries_hit_memory_watermark.sum + - num_reclaimed_expired_sessions.rate + - num_reclaimed_expired_sessions.sum + - slow_query_latency_us.avg + - slow_query_latency_us.p75 + - slow_query_latency_us.p95 + - slow_query_latency_us.p99 + - slow_query_latency_us.p999 + - query_latency_us.avg + - query_latency_us.p75 + - query_latency_us.p95 + - query_latency_us.p99 + - query_latency_us.p999 + - num_auth_failed_sessions_bad_username_password.rate + - num_auth_failed_sessions_bad_username_password.sum + - num_queries.rate + - num_queries.sum + - num_sort_executors.rate + - num_sort_executors.sum + - num_rpc_sent_to_storaged_failed.rate + - num_rpc_sent_to_storaged_failed.sum + - num_auth_failed_sessions.rate + - num_auth_failed_sessions.sum + - num_query_errors.rate + - num_query_errors.sum + - num_killed_queries.rate + - num_killed_queries.sum + - num_query_errors_leader_changes.rate + - num_query_errors_leader_changes.sum + - num_rpc_sent_to_metad.rate + - num_rpc_sent_to_metad.sum + - num_slow_queries.rate + - num_slow_queries.sum + - num_active_sessions.sum + - num_active_queries.sum + - num_sentences.rate + - num_sentences.sum + - num_aggregate_executors.rate + - num_aggregate_executors.sum + - optimizer_latency_us.avg + - optimizer_latency_us.p75 + - optimizer_latency_us.p95 + - optimizer_latency_us.p99 + - optimizer_latency_us.p999 + - num_rpc_sent_to_metad_failed.rate + - num_rpc_sent_to_metad_failed.sum + - num_indexscan_executors.rate + - num_indexscan_executors.sum + - num_opened_sessions.rate + - num_opened_sessions.sum + - num_auth_failed_sessions_out_of_max_allowed.rate + - num_auth_failed_sessions_out_of_max_allowed.sum + - num_rpc_sent_to_storaged.rate + - num_rpc_sent_to_storaged.sum + # mapping and conversion expressions, use thesand aliasField above to calculate metrics value# eg: cores=core1+core2, usage=usage, waitTimeallTime-runningTime + calculates: + - responseTime=responseTime + - num_queries_hit_memory_watermark_rate=num_queries_hit_memory_watermark.rate + - num_queries_hit_memory_watermark_sum=num_queries_hit_memory_watermark.sum + - num_reclaimed_expired_sessions_rate=num_reclaimed_expired_sessions.rate + - num_reclaimed_expired_sessions_sum=num_reclaimed_expired_sessions.sum + - slow_query_latency_us_avg=slow_query_latency_us.avg + - slow_query_latency_us_p75=slow_query_latency_us.p75 + - slow_query_latency_us_p95=slow_query_latency_us.p95 + - slow_query_latency_us_p99=slow_query_latency_us.p99 + - slow_query_latency_us_p999=slow_query_latency_us.p999 + - query_latency_us_avg=query_latency_us.avg + - query_latency_us_p75=query_latency_us.p75 + - query_latency_us_p95=query_latency_us.p95 + - query_latency_us_p99=query_latency_us.p99 + - query_latency_us_p999=query_latency_us.p999 + - num_auth_failed_sessions_bad_username_password_rate=num_auth_failed_sessions_bad_username_password.rate + - num_auth_failed_sessions_bad_username_password_sum=num_auth_failed_sessions_bad_username_password.sum + - num_queries_rate=num_queries.rate + - num_queries_sum=num_queries.sum + - num_sort_executors_rate=num_sort_executors.rate + - num_sort_executors_sum=num_sort_executors.sum + - num_rpc_sent_to_storaged_failed_rate=num_rpc_sent_to_storaged_failed.rate + - num_rpc_sent_to_storaged_failed_sum=num_rpc_sent_to_storaged_failed.sum + - num_auth_failed_sessions_rate=num_auth_failed_sessions.rate + - num_auth_failed_sessions_sum=num_auth_failed_sessions.sum + - num_query_errors_rate=num_query_errors.rate + - num_query_errors_sum=num_query_errors.sum + - num_killed_queries_rate=num_killed_queries.rate + - num_killed_queries_sum=num_killed_queries.sum + - num_query_errors_leader_changes_rate=num_query_errors_leader_changes.rate + - num_query_errors_leader_changes_sum=num_query_errors_leader_changes.sum + - num_rpc_sent_to_metad_rate=num_rpc_sent_to_metad.rate + - num_rpc_sent_to_metad_sum=num_rpc_sent_to_metad.sum + - num_slow_queries_rate=num_slow_queries.rate + - num_slow_queries_sum=num_slow_queries.sum + - num_active_sessions_sum=num_active_sessions.sum + - num_active_queries_sum=num_active_queries.sum + - num_sentences_rate=num_sentences.rate + - num_sentences_sum=num_sentences.sum + - num_aggregate_executors_rate=num_aggregate_executors.rate + - num_aggregate_executors_sum=num_aggregate_executors.sum + - optimizer_latency_us_avg=optimizer_latency_us.avg + - optimizer_latency_us_p75=optimizer_latency_us.p75 + - optimizer_latency_us_p95=optimizer_latency_us.p95 + - optimizer_latency_us_p99=optimizer_latency_us.p99 + - optimizer_latency_us_p999=optimizer_latency_us.p999 + - num_rpc_sent_to_metad_failed_rate=num_rpc_sent_to_metad_failed.rate + - num_rpc_sent_to_metad_failed_sum=num_rpc_sent_to_metad_failed.sum + - num_indexscan_executors_rate=num_indexscan_executors.rate + - num_indexscan_executors_sum=num_indexscan_executors.sum + - num_opened_sessions_rate=num_opened_sessions.rate + - num_opened_sessions_sum=num_opened_sessions.sum + - num_auth_failed_sessions_out_of_max_allowed_rate=num_auth_failed_sessions_out_of_max_allowed.rate + - num_auth_failed_sessions_out_of_max_allowed_sum=num_auth_failed_sessions_out_of_max_allowed.sum + - num_rpc_sent_to_storaged_rate=num_rpc_sent_to_storaged.rate + - num_rpc_sent_to_storaged_sum=num_rpc_sent_to_storaged.sum + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, nginx + protocol: nebulaGraph + nebulaGraph: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^graphPort^_^ + # time period + timePeriod: ^_^timePeriod^_^ + # http url + url: /stats + # timeout + timeout: ^_^timeout^_^ + - name: rocksdb_stats + i18n: + zh-CN: rocksdb 统计数据 + en-US: rocksdb stats + ja-JP: rocksdb統計 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: rocksdb.backup.read.bytes + - field: rocksdb.backup.write.bytes + - field: rocksdb.blobdb.blob.file.bytes.read + - field: rocksdb.blobdb.blob.file.bytes.written + - field: rocksdb.blobdb.blob.file.synced + - field: rocksdb.blobdb.blob.index.evicted.count + - field: rocksdb.blobdb.blob.index.evicted.size + - field: rocksdb.blobdb.blob.index.expired.count + - field: rocksdb.blobdb.blob.index.expired.size + - field: rocksdb.blobdb.bytes.read + - field: rocksdb.blobdb.bytes.written + - field: rocksdb.blobdb.cache.add + - field: rocksdb.blobdb.cache.add.failures + - field: rocksdb.blobdb.cache.bytes.read + - field: rocksdb.blobdb.cache.bytes.write + - field: rocksdb.blobdb.cache.hit + - field: rocksdb.blobdb.cache.miss + - field: rocksdb.blobdb.fifo.bytes.evicted + - field: rocksdb.blobdb.fifo.num.files.evicted + - field: rocksdb.blobdb.fifo.num.keys.evicted + - field: rocksdb.blobdb.gc.bytes.expired + - field: rocksdb.blobdb.gc.bytes.overwritten + - field: rocksdb.blobdb.gc.bytes.relocated + - field: rocksdb.blobdb.gc.failures + - field: rocksdb.blobdb.gc.num.files + - field: rocksdb.blobdb.gc.num.keys.expired + - field: rocksdb.blobdb.gc.num.keys.overwritten + - field: rocksdb.blobdb.gc.num.keys.relocated + - field: rocksdb.blobdb.gc.num.new.files + - field: rocksdb.blobdb.num.get + - field: rocksdb.blobdb.num.keys.read + - field: rocksdb.blobdb.num.keys.written + - field: rocksdb.blobdb.num.multiget + - field: rocksdb.blobdb.num.next + - field: rocksdb.blobdb.num.prev + - field: rocksdb.blobdb.num.put + - field: rocksdb.blobdb.num.seek + - field: rocksdb.blobdb.num.write + - field: rocksdb.blobdb.write.blob + - field: rocksdb.blobdb.write.blob.ttl + - field: rocksdb.blobdb.write.inlined + - field: rocksdb.blobdb.write.inlined.ttl + - field: rocksdb.block.cache.add + - field: rocksdb.block.cache.add.failures + - field: rocksdb.block.cache.add.redundant + - field: rocksdb.block.cache.bytes.read + - field: rocksdb.block.cache.bytes.write + - field: rocksdb.block.cache.compression.dict.add + - field: rocksdb.block.cache.compression.dict.add.redundant + - field: rocksdb.block.cache.compression.dict.bytes.evict + - field: rocksdb.block.cache.compression.dict.bytes.insert + - field: rocksdb.block.cache.compression.dict.hit + - field: rocksdb.block.cache.compression.dict.miss + - field: rocksdb.block.cache.data.add + - field: rocksdb.block.cache.data.add.redundant + - field: rocksdb.block.cache.data.bytes.insert + - field: rocksdb.block.cache.data.hit + - field: rocksdb.block.cache.data.miss + - field: rocksdb.block.cache.filter.add + - field: rocksdb.block.cache.filter.add.redundant + - field: rocksdb.block.cache.filter.bytes.evict + - field: rocksdb.block.cache.filter.bytes.insert + - field: rocksdb.block.cache.filter.hit + - field: rocksdb.block.cache.filter.miss + - field: rocksdb.block.cache.hit + - field: rocksdb.block.cache.index.add + - field: rocksdb.block.cache.index.add.redundant + - field: rocksdb.block.cache.index.bytes.evict + - field: rocksdb.block.cache.index.bytes.insert + - field: rocksdb.block.cache.index.hit + - field: rocksdb.block.cache.index.miss + - field: rocksdb.block.cache.miss + - field: rocksdb.block.cachecompressed.add + - field: rocksdb.block.cachecompressed.add.failures + - field: rocksdb.block.cachecompressed.hit + - field: rocksdb.block.cachecompressed.miss + - field: rocksdb.block.checksum.compute.count + - field: rocksdb.bloom.filter.full.positive + - field: rocksdb.bloom.filter.full.true.positive + - field: rocksdb.bloom.filter.micros + - field: rocksdb.bloom.filter.prefix.checked + - field: rocksdb.bloom.filter.prefix.useful + - field: rocksdb.bloom.filter.useful + - field: rocksdb.bytes.read + - field: rocksdb.bytes.written + - field: rocksdb.cold.file.read.bytes + - field: rocksdb.cold.file.read.count + - field: rocksdb.compact.read.bytes + - field: rocksdb.compact.read.marked.bytes + - field: rocksdb.compact.read.periodic.bytes + - field: rocksdb.compact.read.ttl.bytes + - field: rocksdb.compact.write.bytes + - field: rocksdb.compact.write.marked.bytes + - field: rocksdb.compact.write.periodic.bytes + - field: rocksdb.compact.write.ttl.bytes + - field: rocksdb.compaction.cancelled + - field: rocksdb.compaction.key.drop.new + - field: rocksdb.compaction.key.drop.obsolete + - field: rocksdb.compaction.key.drop.range_del + - field: rocksdb.compaction.key.drop.user + - field: rocksdb.compaction.optimized.del.drop.obsolete + - field: rocksdb.compaction.range_del.drop.obsolete + - field: rocksdb.db.iter.bytes.read + - field: rocksdb.db.mutex.wait.micros + - field: rocksdb.error.handler.autoresume.count + - field: rocksdb.error.handler.autoresume.retry.total.count + - field: rocksdb.error.handler.autoresume.success.count + - field: rocksdb.error.handler.bg.errro.count + - field: rocksdb.error.handler.bg.io.errro.count + - field: rocksdb.error.handler.bg.retryable.io.errro.count + - field: rocksdb.files.deleted.immediately + - field: rocksdb.files.marked.trash + - field: rocksdb.filter.operation.time.nanos + - field: rocksdb.flush.write.bytes + - field: rocksdb.getupdatessince.calls + - field: rocksdb.hot.file.read.bytes + - field: rocksdb.hot.file.read.count + - field: rocksdb.l0.hit + - field: rocksdb.l0.num.files.stall.micros + - field: rocksdb.l0.slowdown.micros + - field: rocksdb.l1.hit + - field: rocksdb.l2andup.hit + - field: rocksdb.last.level.read.bytes + - field: rocksdb.last.level.read.count + - field: rocksdb.memtable.compaction.micros + - field: rocksdb.memtable.garbage.bytes.at.flush + - field: rocksdb.memtable.hit + - field: rocksdb.memtable.miss + - field: rocksdb.memtable.payload.bytes.at.flush + - field: rocksdb.merge.operation.time.nanos + - field: rocksdb.multiget.coroutine.count + - field: rocksdb.no.file.closes + - field: rocksdb.no.file.errors + - field: rocksdb.no.file.opens + - field: rocksdb.non.last.level.read.bytes + - field: rocksdb.non.last.level.read.count + - field: rocksdb.num.iterator.created + - field: rocksdb.num.iterator.deleted + - field: rocksdb.num.iterators + - field: rocksdb.number.block.compressed + - field: rocksdb.number.block.decompressed + - field: rocksdb.number.block.not_compressed + - field: rocksdb.number.db.next + - field: rocksdb.number.db.next.found + - field: rocksdb.number.db.prev + - field: rocksdb.number.db.prev.found + - field: rocksdb.number.db.seek + - field: rocksdb.number.db.seek.found + - field: rocksdb.number.deletes.filtered + - field: rocksdb.number.direct.load.table.properties + - field: rocksdb.number.iter.skip + - field: rocksdb.number.keys.read + - field: rocksdb.number.keys.updated + - field: rocksdb.number.keys.written + - field: rocksdb.number.merge.failures + - field: rocksdb.number.multiget.bytes.read + - field: rocksdb.number.multiget.get + - field: rocksdb.number.multiget.keys.found + - field: rocksdb.number.multiget.keys.read + - field: rocksdb.number.rate_limiter.drains + - field: rocksdb.number.reseeks.iteration + - field: rocksdb.number.superversion_acquires + - field: rocksdb.number.superversion_cleanups + - field: rocksdb.number.superversion_releases + - field: rocksdb.persistent.cache.hit + - field: rocksdb.persistent.cache.miss + - field: rocksdb.rate.limit.delay.millis + - field: rocksdb.read.amp.estimate.useful.bytes + - field: rocksdb.read.amp.total.read.bytes + - field: rocksdb.remote.compact.read.bytes + - field: rocksdb.remote.compact.write.bytes + - field: rocksdb.row.cache.hit + - field: rocksdb.row.cache.miss + - field: rocksdb.secondary.cache.hits + - field: rocksdb.sim.block.cache.hit + - field: rocksdb.sim.block.cache.miss + - field: rocksdb.stall.micros + - field: rocksdb.txn.get.tryagain + - field: rocksdb.txn.overhead.duplicate.key + - field: rocksdb.txn.overhead.mutex.old.commit.map + - field: rocksdb.txn.overhead.mutex.prepare + - field: rocksdb.txn.overhead.mutex.snapshot + - field: rocksdb.verify_checksum.read.bytes + - field: rocksdb.wal.bytes + - field: rocksdb.wal.synced + - field: rocksdb.warm.file.read.bytes + - field: rocksdb.warm.file.read.count + - field: rocksdb.write.other + - field: rocksdb.write.self + - field: rocksdb.write.timeout + - field: rocksdb.write.wal + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, nginx + protocol: nebulaGraph + nebulaGraph: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^storagePort^_^ + # http url + url: /rocksdb_stats + # timeout + timeout: ^_^timeout^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-nebula_graph_cluster.yml b/hertzbeat-manager/src/main/resources/define/app-nebula_graph_cluster.yml new file mode 100644 index 0000000..8e087f4 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-nebula_graph_cluster.yml @@ -0,0 +1,487 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring mid-middleware +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: nebula_graph_cluster +# The app api i18n name +name: + zh-CN: NebulaGraph集群 + en-US: NebulaGraph Cluster + ja-JP: NebulaGraphクラスター +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat通过查询使用NGQL或OpenCypher查询 NebulaGraph 集群的基础信息,节点信息等指标。 + en-US: Hertzbeat uses NGQL or OpenCypher to query the basic information, node information and other metrics of the NebulaGraph cluster. + zh-TW: Hertzbeat透過查詢使用NGQL或OpenCypher查詢 NebulaGraph 叢集的基礎訊息,節點資訊等指標。 + ja-JP: Hertzbeatは、NGQLかOpenCypherをクエリしてNebulaGraphクラスターのメトリクスを監視します。 +# The description and help of this monitoring type +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/nebulagraph_cluster + en-US: https://hertzbeat.apache.org/docs/help/nebulaGraph_cluster +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: graphPort + # name-param field display i18n name + name: + zh-CN: graph端口 + en-US: graphPort + ja-JP: graphポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 9669 + + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is number, range is required + # required-true or false + required: true + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input type) + type: password + # when type is number, range is required + # required-true or false + required: true + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 + + +# collect metrics config list +metrics: + # metrics - base info + - name: base_info + i18n: + zh-CN: 基础信息 + en-US: Base info + ja-JP: 基礎情報 + priority: 0 + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: charset + type: 1 + i18n: + zh-CN: 字符集 + en-US: Charset + ja-JP: 文字セット + - field: Collation + type: 1 + i18n: + zh-CN: 排序规则 + en-US: collation + ja-JP: 照合 + - field: metaLeader + type: 1 + i18n: + zh-CN: Meta Leader + en-US: Meta Leader + ja-JP: Metaリーダー + + aliasFields: + - responseTime + - Charset + - Collation + - Meta Leader + calculates: + - charset=Charset + - collation=Collation + - metaLeader=Meta Leader + protocol: ngql + ngql: + host: ^_^host^_^ + username: ^_^username^_^ + password: ^_^password^_^ + port: ^_^graphPort^_^ + parseType: oneRow + commands: + - SHOW COLLATION; + - SHOW META LEADER; + timeout: ^_^timeout^_^ + # metrics - sessions + - name: session + i18n: + zh-CN: Session + en-US: Session + ja-JP: セッション + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + fields: + - field: session + type: 0 + i18n: + zh-CN: Session数量 + en-US: Session + ja-JP: セッション数量 + - field: running_query_count + type: 1 + i18n: + zh-CN: 正在执行的查询数量 + en-US: Running query + ja-JP: 実行中のクエリ数 + + aliasFields: + - session + - running_query_count + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, nginx + protocol: ngql + ngql: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # http port + port: ^_^graphPort^_^ + parseType: filterCount + commands: + - session#show sessions## + - running_query_count#show queries#Status#RUNNING + # timeout + timeout: ^_^timeout^_^ + # metrics - jobs + - name: jobs + i18n: + zh-CN: 后台任务 + en-US: Jobs + ja-JP: タスク + priority: 2 + fields: + - field: queue_jobs + type: 0 + i18n: + zh-CN: 等待中的任务 + en-US: Queue jobs + ja-JP: 待機中のタスク数 + - field: running_jobs + type: 0 + i18n: + zh-CN: 执行中的任务 + en-US: Running jobs + ja-JP: 実行中のタスク数 + + aliasFields: + - queue_jobs + - running_jobs + protocol: ngql + ngql: + host: ^_^host^_^ + username: ^_^username^_^ + password: ^_^password^_^ + port: ^_^graphPort^_^ + parseType: oneRow + commands: + - show jobs; + timeout: ^_^timeout^_^ + # metrics - node_info + - name: node_info + i18n: + zh-CN: 节点信息 + en-US: Cluster node info + ja-JP: クラスターノード情報 + priority: 3 + fields: + - field: total_storage_node + type: 0 + i18n: + zh-CN: Storage节点 + en-US: Total storage node + ja-JP: ストレージノード総数 + - field: offline_storage_node + type: 0 + i18n: + zh-CN: 离线Storage节点 + en-US: Offline storage node + ja-JP: オフラインのストレージノード + + - field: total_meta_node + type: 0 + i18n: + zh-CN: Meta节点 + en-US: Total meta node + ja-JP: Metaノード総数 + - field: offline_meta_node + type: 0 + i18n: + zh-CN: 离线Meta节点 + en-US: Offline meta node + ja-JP: オフラインのMetaノード + + - field: total_graph_node + type: 0 + i18n: + zh-CN: Graph节点 + en-US: Total graph node + ja-JP: Graphノード総数 + - field: offline_graph_node + type: 0 + i18n: + zh-CN: 离线Graph节点 + en-US: Offline graph node + ja-JP: オフラインのGraphノード + + aliasFields: + - total_storage_node + - offline_storage_node + - total_meta_node + - offline_meta_node + - total_graph_node + - offline_graph_node + calculates: + - responseTime=responseTime + protocol: ngql + ngql: + host: ^_^host^_^ + username: ^_^username^_^ + password: ^_^password^_^ + port: ^_^graphPort^_^ + parseType: filterCount + commands: + - total_storage_node#show hosts#Status#ONLINE + - offline_storage_node#show hosts#Status#OFFLINE + - total_meta_node#show hosts meta#Status#ONLINE + - offline_meta_node#show hosts meta#Status#OFFLINE + - total_graph_node#show hosts graph#Status#ONLINE + - offline_graph_node#show hosts graph#Status#OFFLINE + timeout: ^_^timeout^_^ + # metrics - storage + - name: storage + i18n: + zh-CN: Storage节点 + en-US: Storage Nodes + ja-JP: ストレージノード + priority: 4 + fields: + - field: host + type: 1 + i18n: + zh-CN: Host + en-US: Host + ja-JP: ホスト + - field: port + type: 0 + i18n: + zh-CN: Port + en-US: Port + ja-JP: ポート + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: STATUS + ja-JP: ステータス + - field: leaderCount + type: 0 + i18n: + zh-CN: Leader数量 + en-US: Leader Count + ja-JP: リーダー数量 + - field: leaderDistribution + type: 1 + i18n: + zh-CN: Leader分布 + en-US: Leader Distribution + ja-JP: リーダー分布 + - field: partitionDistribution + type: 1 + i18n: + zh-CN: Partition分布 + en-US: Partition Distribution + ja-JP: パーティション分布 + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + + aliasFields: + - Host + - Port + - Status + - Leader count + - Leader distribution + - Partition distribution + - Version + calculates: + - host=Host + - port=Port + - status=Status + - leaderCount=Leader count + - leaderDistribution=Leader distribution + - partitionDistribution=Partition distribution + - version=Version + protocol: ngql + ngql: + host: ^_^host^_^ + username: ^_^username^_^ + password: ^_^password^_^ + port: ^_^graphPort^_^ + parseType: multiRow + commands: + - show hosts; + timeout: ^_^timeout^_^ + # metrics - meta + - name: meta + i18n: + zh-CN: Meta节点 + en-US: Meta Nodes + ja-JP: Metaノード + priority: 5 + fields: + - field: host + type: 1 + i18n: + zh-CN: Host + en-US: Host + ja-JP: ホスト + - field: port + type: 0 + i18n: + zh-CN: Port + en-US: Port + ja-JP: ポート + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: STATUS + ja-JP: ステータス + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + + aliasFields: + - Host + - Port + - Status + - Version + calculates: + - host=Host + - port=Port + - status=Status + - version=Version + protocol: ngql + ngql: + host: ^_^host^_^ + username: ^_^username^_^ + password: ^_^password^_^ + port: ^_^graphPort^_^ + parseType: multiRow + commands: + - show hosts meta; + timeout: ^_^timeout^_^ + # metrics - graph + - name: graph + i18n: + zh-CN: Graph节点 + en-US: Graph Nodes + ja-JP: Graphノード + priority: 6 + fields: + - field: host + type: 1 + i18n: + zh-CN: Host + en-US: Host + ja-JP: ホスト + - field: port + type: 0 + i18n: + zh-CN: Port + en-US: Port + ja-JP: ポート + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: STATUS + ja-JP: ステータス + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + + aliasFields: + - Host + - Port + - Status + - Version + calculates: + - host=Host + - port=Port + - status=Status + - version=Version + protocol: ngql + ngql: + host: ^_^host^_^ + username: ^_^username^_^ + password: ^_^password^_^ + port: ^_^graphPort^_^ + parseType: multiRow + commands: + - show hosts graph; + timeout: ^_^timeout^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-netease_mailbox.yml b/hertzbeat-manager/src/main/resources/define/app-netease_mailbox.yml new file mode 100644 index 0000000..2afe369 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-netease_mailbox.yml @@ -0,0 +1,446 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: service +# Monitoring application type name (consistent with file name) eg: linux windows tomcat mysql aws... +app: netease_mailbox +# The app api i18n name +name: + zh-CN: 网易邮箱监控 + en-US: NetEase Mailbox + ja-JP: NetEase電子メール +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 IMAP 协议对网易邮箱的详细信息进行采集监控。<br>您可以点击“<i>新建 网易邮箱</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses IMAP protocol to collect detailed metrics of NetEase mailbox. <br>You can click "<i>New NetEase Mailbox</i>" and configure it, or select "<i>More Operations</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 IMAP 協議對網易郵箱的詳細信息進行採集監控。<br>您可以點擊“<i>新建 网易邮箱</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: HertzBeat は IMAP プロトコルを介してNetEase電子メールの詳細情報を監視します。<br>「<i>新規 NetEase電子メール</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/imap + en-US: https://hertzbeat.apache.org/docs/help/imap +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL + # When the type is boolean, the frontend will display a switch for it. + type: boolean + # required-true or false + required: false + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 + # field-param field key + - field: email + # name-param field display i18n name + name: + zh-CN: IMAP 邮箱地址 + en-US: IMAP Address + ja-JP: IMAPアドレス + # type-param field type(most mapping the html input type) The type "text" belongs to a text input field. + type: text + # required-true or false + required: true + # field-param field key + - field: authorize + # name-param field display i18n name + name: + zh-CN: 授权码 + en-US: Authorize Code + ja-JP: 認証コード + # type-param field type(most mapping the html input type) The type "text" belongs to a text input field. + type: text + # required-true or false + required: true + +# collect metrics config list +metrics: + # metrics - available + - name: available + i18n: + zh-CN: 可用性 + en-US: Available + ja-JP: 可用性 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: placeholder + + - name: inboxInfo + i18n: + zh-CN: 收件箱信息 + en-US: Inbox info + ja-JP: 受信箱情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: allMessageCount + type: 0 + i18n: + zh-CN: 邮件总数 + en-US: Total message count + ja-JP: メッセージ総数 + - field: recentMessageCount + type: 0 + i18n: + zh-CN: 最近收到邮件总数 + en-US: Recent message count + ja-JP: 最新メッセージ総数 + - field: unseenMessageCount + type: 0 + i18n: + zh-CN: 未读邮件总数 + en-US: Unread message count + ja-JP: 未読メッセージ総数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - INBOXTotalMessageCount + - INBOXRecentMessageCount + - INBOXUnseenMessageCount + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + calculates: + - allMessageCount = INBOXTotalMessageCount + - recentMessageCount = INBOXRecentMessageCount + - unseenMessageCount = INBOXUnseenMessageCount + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: INBOX + + - name: sentInfo + i18n: + zh-CN: 已发送信息 + en-US: Sent info + ja-JP: 送信情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: allMessageCount + type: 0 + i18n: + zh-CN: 邮件总数 + en-US: Total message count + ja-JP: 送信したメッセージ総数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - 已发送TotalMessageCount + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + calculates: + - allMessageCount = 已发送TotalMessageCount + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: 已发送 + + - name: draftsInfo + i18n: + zh-CN: 草稿箱信息 + en-US: Drafts info + ja-JP: ドラフトボックス情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: allMessageCount + type: 0 + i18n: + zh-CN: 邮件总数 + en-US: Total message count + ja-JP: ドラフトメッセージ総数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - 草稿箱TotalMessageCount + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + calculates: + - allMessageCount = 草稿箱TotalMessageCount + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: 草稿箱 + + - name: deletedMessagesInfo + i18n: + zh-CN: 已删除信息 + en-US: Deleted Messages info + ja-JP: 削除済みメッセージ情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: allMessageCount + type: 0 + i18n: + zh-CN: 邮件总数 + en-US: Total message count + ja-JP: 削除済みメッセージ総数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - 已删除TotalMessageCount + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + calculates: + - allMessageCount = 已删除TotalMessageCount + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: 已删除 + + - name: JunkInfo + i18n: + zh-CN: 垃圾箱信息 + en-US: Junk info + ja-JP: 迷惑メールボックス情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: allMessageCount + type: 0 + i18n: + zh-CN: 邮件总数 + en-US: Total message count + ja-JP: 迷惑メッセージ総数 + - field: recentMessageCount + type: 0 + i18n: + zh-CN: 最近收到邮件总数 + en-US: Recent message count + ja-JP: 最新の迷惑メッセージ総数 + - field: unseenMessageCount + type: 0 + i18n: + zh-CN: 未读邮件总数 + en-US: Unread message count + ja-JP: 未読の迷惑メッセージ総数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - 垃圾邮件TotalMessageCount + - 垃圾邮件RecentMessageCount + - 垃圾邮件UnseenMessageCount + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + calculates: + - allMessageCount = 垃圾邮件TotalMessageCount + - recentMessageCount = 垃圾邮件RecentMessageCount + - unseenMessageCount = 垃圾邮件UnseenMessageCount + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: 垃圾邮件 + + - name: virusInfo + i18n: + zh-CN: 病毒邮件信息 + en-US: Virus Info + ja-JP: ウイルスメールボックス情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: allMessageCount + type: 0 + i18n: + zh-CN: 邮件总数 + en-US: Total message count + ja-JP: ウイルスメッセージ総数 + - field: recentMessageCount + type: 0 + i18n: + zh-CN: 最近收到邮件总数 + en-US: Recent message count + ja-JP: 最新のウイルスメッセージ総数 + - field: unseenMessageCount + type: 0 + i18n: + zh-CN: 未读邮件总数 + en-US: Unread message count + ja-JP: 未読のウイルスメッセージ総数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - 病毒邮件TotalMessageCount + - 病毒邮件RecentMessageCount + - 病毒邮件UnseenMessageCount + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + calculates: + - allMessageCount = 病毒邮件TotalMessageCount + - recentMessageCount = 病毒邮件RecentMessageCount + - unseenMessageCount = 病毒邮件UnseenMessageCount + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: 病毒邮件 diff --git a/hertzbeat-manager/src/main/resources/define/app-nginx.yml b/hertzbeat-manager/src/main/resources/define/app-nginx.yml new file mode 100644 index 0000000..ed13e74 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-nginx.yml @@ -0,0 +1,281 @@ +# 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. + +# The monitoring type category: service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring mid-middleware webserver-web server cache-cache cn-cloud native network-network monitoring, etc. +category: service +# The monitoring type name(consistent with the file name) eg: linux windows tomcat mysql aws... +app: nginx +# The app api i18n name +name: + zh-CN: Nginx服务器 + en-US: Nginx Server + ja-JP: Nginxサーバー +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://www.tecmint.com/enable-nginx-status-page/'> Nginx </a> 通过配置 ngx_http_stub_status_module 对 nginx 的通用性能指标(nginx_status、req_status等)进行采集监控。<br>您可以点击“<i>新建 nginx</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://www.tecmint.com/enable-nginx-status-page/'> Nginx </a> to configure ngx_http_stub_status_module for collecting general metrics of Nginx (nginx_status, req_status.). <br>You can click "<i>New Nginx</i>" and configure it, or select "<i>More Operations</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://www.tecmint.com/enable-nginx-status-page/'> Nginx </a> 通過配置 ngx_http_stub_status_module 對 Nginx 數據庫的通用性能指標(nginx信息、请求信息等)進行采集監控。<br>您可以點擊“<i>新建 Nginx</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: HertzBeat は、<a class='help_module_content' href='https://www.tecmint.com/enable-nginx-status-page/'>Nginx</a> の ngx_http_stub_status_module を設定することで、nginx_status や req_status などの一般的なパフォーマンス指標を収集・監視します。<br>「<i>新規 nginx 作成</i>」をクリックして設定を行うか、「<i>その他の操作</i>」から既存の設定をインポートすることができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/nginx + en-US: https://hertzbeat.apache.org/docs/help/nginx +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 80 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL利用 + # When the type is boolean, the frontend will display a switch for it. + type: boolean + # required-true or false + required: false + +# collect metrics config list +metrics: + # metrics - available + - name: available + i18n: + zh-CN: 可用性 + en-US: Available + ja-JP: 可用性 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: nginx + # Specific collection configuration when protocol is http protocol + nginx: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # ssl + ssl: ^_^ssl^_^ + + - name: nginx_status + i18n: + zh-CN: Nginx 状态信息 + en-US: Nginx Status + ja-JP: Nginx 状態情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + fields: + - field: accepts + type: 0 + i18n: + zh-CN: 接受连接数 + en-US: Accepts + ja-JP: 受けたクライアント接続の総数 + - field: handled + type: 0 + i18n: + zh-CN: 成功处理连接数 + en-US: Handled + ja-JP: 処理された接続の総数 + - field: active + type: 0 + i18n: + zh-CN: 当前活跃连接数 + en-US: Active + ja-JP: 現在アクティブなクライアント接続数 + - field: dropped + type: 0 + i18n: + zh-CN: 已丢弃连接数 + en-US: Dropped + ja-JP: 中断したクライアント接続数 + - field: requests + type: 0 + i18n: + zh-CN: 客户端请求数 + en-US: Requests + ja-JP: クライアントリクエストの総数 + - field: reading + type: 0 + i18n: + zh-CN: 正在执行读操作的连接数 + en-US: Reading + ja-JP: 読み込み中の接続数 + - field: writing + type: 0 + i18n: + zh-CN: 正在执行写操作的连接数 + en-US: Writing + ja-JP: 書き込み中の接続数 + - field: waiting + type: 0 + i18n: + zh-CN: 正在等待的连接数 + en-US: Waiting + ja-JP: 待機中の接続数 + # Alias field list, used to identify metrics in the query result + aliasFields: + - accepts + - handled + - active + - dropped + - requests + - reading + - writing + - waiting + # A list of calculation scripts for metric values. + calculates: + - dropped=accepts - handled + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, nginx + protocol: nginx + # the config content when protocol is http + nginx: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # http url + url: /nginx-status + # ssl + ssl: ^_^ssl^_^ + + - name: req_status + i18n: + zh-CN: 请求详细信息 + en-US: Req Status + ja-JP: リクエストの詳細情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + fields: + - field: zone_name + type: 1 + i18n: + zh-CN: 分组类别 + en-US: Zone Name + ja-JP: ゾーン名 + - field: key + type: 1 + i18n: + zh-CN: 分组名称 + en-US: Key + ja-JP: グループの識別名 + - field: max_active + type: 0 + i18n: + zh-CN: 最大并发连接数 + en-US: Max Active + ja-JP: 最大の同時実行の接続数 + - field: max_bw + type: 0 + unit: KB + i18n: + zh-CN: 最大带宽 + en-US: Max BW + ja-JP: 最大帯域幅 + - field: traffic + type: 0 + unit: KB + i18n: + zh-CN: 总流量 + en-US: Traffic + ja-JP: 通信量 + - field: requests + type: 0 + i18n: + zh-CN: 总请求数 + en-US: Requests + ja-JP: 累積したリクエストの合計 + - field: active + type: 0 + i18n: + zh-CN: 当前并发连接数 + en-US: Active + ja-JP: 現在のアクティブな同時実行接続数 + - field: bandwidth + type: 0 + unit: KB + i18n: + zh-CN: 当前带宽 + en-US: Bandwidth + ja-JP: 現在の帯域幅 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: nginx + # the config content when protocol is http + nginx: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # http url + url: /req-status + # ssl + ssl: ^_^ssl^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-ntp.yml b/hertzbeat-manager/src/main/resources/define/app-ntp.yml new file mode 100644 index 0000000..e2602de --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-ntp.yml @@ -0,0 +1,144 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: ntp +# The app api i18n name +name: + zh-CN: NTP服务器 + en-US: NTP Server + ja-JP: NTPサーバー +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 NTP 服务的(响应时间、时间戳、时间、偏移量、延迟、版本号、模式、层级、参考ID、精度)相关指标进行监测。<br>您可以点击 “<i>新建 NTP服务器</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat monitors the relevant metrics of NTP services, including response time, timestamp, time, offset, delay, version number, mode, hierarchy, reference ID, and accuracy. <br>You can click "<i>New NTP Server</i>" and configure it, or select "<i>More Action</i>" to import the existing configuration. + zh-TW: HertzBeat 對 NTP 服務的(響應時間、時間戳、時間、偏移量、延遲、版本號、模式、層級、參考ID、精度)相關指標進行監測。<br>您可以點擊 “<i>新建 NTP服務器</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は NTPサーバーの一般的なメトリクスを監視します。<br>「<i>新規 NTPサーバー</i>」をクリックしてパラメタを設定した後、新規することができます。 +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: NTP服务Host + en-US: NTP Host + ja-JP: NTPホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - summary + - name: summary + i18n: + zh-CN: 概要 + en-US: Summary + ja-JP: 概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: time + type: 0 + unit: ms + i18n: + zh-CN: 时间戳 + en-US: Timestamp + ja-JP: タイムスタンプ + - field: date + type: 1 + i18n: + zh-CN: 时间 + en-US: Date + ja-JP: 時間 + - field: offset + type: 0 + unit: ms + i18n: + zh-CN: 偏移量 + en-US: Offset + ja-JP: オフセット + - field: delay + type: 0 + unit: ms + i18n: + zh-CN: 延迟 + en-US: Delay + ja-JP: 遅延 + - field: version + type: 0 + i18n: + zh-CN: 版本号 + en-US: Version + ja-JP: バージョン + - field: mode + type: 0 + i18n: + zh-CN: 模式 + en-US: Mode + ja-JP: モード + - field: stratum + type: 0 + i18n: + zh-CN: 层级 + en-US: Stratum + ja-JP: 階層 + - field: referenceId + type: 1 + i18n: + zh-CN: 参考ID + en-US: ReferenceId + ja-JP: 参照ID + - field: precision + type: 0 + i18n: + zh-CN: 精度 + en-US: Precision + ja-JP: 精度 + + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ntp + # Specific collection configuration when protocol is telnet protocol + ntp: + # telnet host + host: ^_^host^_^ + # timeout + timeout: ^_^timeout^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-nvidia.yml b/hertzbeat-manager/src/main/resources/define/app-nvidia.yml new file mode 100644 index 0000000..611489b --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-nvidia.yml @@ -0,0 +1,329 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: llm +# The monitoring type eg: linux windows tomcat mysql aws... +app: nvidia +# The monitoring i18n name +name: + zh-CN: NVIDIA + en-US: NVIDIA + ja-JP: NVIDIA +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 NVIDIA GPU显卡的通用性能指标进行采集监控。<br>您可以点击“<i>新建 NVIDIA</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat uses the <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'>SSH protocol</a> to collect and monitor general performance metrics of NVIDIA GPUs. <br> You can click " <i>Create NVIDIA</i> " to add and configure parameters such as HOST, port, account, etc., supporting SSH account password or key authentication. Alternatively, you can select " <i>More Actions</i> " to import an existing configuration. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'>SSH 協議</a> 對 NVIDIA GPU 顯卡的通用性能指標進行採集監控。<br>您可以點擊“<i>新建 NVIDIA</i>”並配置 HOST、端口、帳戶等相關參數進行添加,支持 SSH 帳戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> NVIDIAの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 NVIDIA</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/nvidia/ + en-US: https://hertzbeat.apache.org/docs/help/nvidia/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 显卡基本信息 + en-US: Basic Information + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: index + type: 1 + label: true + i18n: + zh-CN: 显卡索引 + en-US: Host Name + ja-JP: 番号 + - field: name + type: 1 + i18n: + zh-CN: 显卡名称 + en-US: System Version + ja-JP: 名前 + - field: utilization_gpu + type: 0 + unit: '%' + i18n: + zh-CN: GPU利用率 + en-US: GPU Utilization + ja-JP: GPU使用率 + - field: utilization_memory + type: 0 + unit: '%' + i18n: + zh-CN: 显存利用率 + en-US: Memory Utilization + ja-JP: メモリ使用率 + - field: memory_total + type: 1 + unit: 'MiB' + i18n: + zh-CN: 总显存 + en-US: Total Memory + ja-JP: メモリ容量 + - field: memory_used + type: 0 + unit: 'MiB' + i18n: + zh-CN: 已用显存 + en-US: Used Memory + ja-JP: 使用したメモリ + - field: memory_free + type: 0 + unit: 'MiB' + i18n: + zh-CN: 空闲显存 + en-US: Free Memory + ja-JP: 利用可能メモリ + - field: temperature_gpu + type: 1 + unit: '°C' + i18n: + zh-CN: 显卡温度 + en-US: GPU Temperature + ja-JP: GPU温度 + aliasFields: + - index + - name + - utilization.gpu [%] + - utilization.memory [%] + - memory.total [MiB] + - memory.used [MiB] + - memory.free [MiB] + - temperature_gpu + calculates: + - index + - name + - utilization_gpu = utilization.gpu [%] + - utilization_memory = utilization.memory [%] + - memory_total = memory.total [MiB] + - memory_used = memory.used [MiB] + - memory_free = memory.free [MiB] + - temperature_gpu = temperature.gpu + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: nvidia-smi --query-gpu=index,name,utilization.gpu,utilization.memory,memory.total,memory.used,memory.free,temperature.gpu --format=csv,nounits | sed 's/ *, */,/g' | sed 's/ / /g' | sed 's/,/ /g' + # ssh response data parse type: oneRow, multiRow + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-oceanbase.yml b/hertzbeat-manager/src/main/resources/define/app-oceanbase.yml new file mode 100644 index 0000000..06a91ec --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-oceanbase.yml @@ -0,0 +1,389 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type +app: oceanbase +# The monitoring i18n name +name: + zh-CN: OceanBase数据库 + en-US: OceanBase DB + ja-JP: OceanBaseデータベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 OceanBase 数据库的通用性能指标(系统信息、性能状态、Innodb、缓存、事物、用户线程、慢SQL等)进行采集监控,支持版本为 OceanBase 4.0+。<br>您可以点击“<i>新建 OceanBase 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC Protocol</a> to configure SQL for collecting general metrics of OceanBase database (system information, performance status, Innodb, cache, things, user threads, slow SQL, etc.). Supported version is OceanBase 4.0+. <br>You can click "<i>New OceanBase Database</i>" and configure it, or select "<i>More Operations</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 協議</a> 通過配置 SQL 對 OceanBase 數據庫的通用性能指標(系統信息、性能狀態、Innodb、緩存、事物、用戶線程、慢SQL等)進行采集監控,支持版本爲 OceanBase 4.0+。<br>您可以點擊“<i>新建 OceanBase 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: HertzBeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC プロトコル</a> を使用して、SQL を設定することで OceanBase データベースの一般的なパフォーマンスのメトリクス(システム情報、パフォーマンス状態、Innodb、キャッシュ、トランザクション、ユーザー スレッド、スロー SQL など)を収集および監視します。対応バージョンは OceanBase 4.0+ です。<br>「<i>新規 OceanBase データベース</i>」をクリックして設定するか、「<i>その他の操作</i>」を選択して既存の設定をインポートすることができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/oceanbase + en-US: https://hertzbeat.apache.org/docs/help/oceanbase +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: ターゲットホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 2881 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 + # field-param field key + - field: database + # name-param field display i18n name + name: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + # type-param field type(most mapping the html input tag) + type: text + # required-true or false + required: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input tag) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: url + # name-param field display i18n name + name: + zh-CN: URL + en-US: URL + ja-JP: URL + # type-param field type(most mapping the html input tag) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + +# collect metrics config list +metrics: + # metrics - basic + - name: basic + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + i18n: + zh-CN: 基础 信息 + en-US: Basic Info + ja-JP: 基本情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: version + type: 1 + label: true + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + - field: datadir + type: 1 + i18n: + zh-CN: 存储目录 + en-US: DataDir + ja-JP: データディレクトリ + - field: max_connections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - version + - version_compile_os + - version_compile_machine + - datadir + - max_connections + # (optional)mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - datadir=datadir + - max_connections=max_connections + - version=version+"_"+version_compile_os+"_"+version_compile_machine + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # OceanBase host: ipv4 ipv6 host + host: ^_^host^_^ + # OceanBase port + port: ^_^port^_^ + # database platform name + platform: mysql + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # database name + database: ^_^database^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + # sql + sql: show global variables where Variable_name like 'version%' or Variable_name = 'max_connections' or Variable_name = 'datadir' ; + # JDBC url + url: ^_^url^_^ + # metrics - tenant + - name: tenant + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + i18n: + zh-CN: 租户 信息 + en-US: Tenant Info + ja-JP: テナント情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: tenant_id + type: 1 + label: true + i18n: + zh-CN: 租户id + en-US: TenantId + ja-JP: テナントID + - field: tenant_name + type: 1 + i18n: + zh-CN: 租户名称 + en-US: tenantName + ja-JP: テナント名 + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: status + ja-JP: ステータス + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - tenant_id + - tenant_name + - status + # (optional)mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - tenant_id=tenant_id + - tenant_name=tenant_name + - status=status + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # OceanBase host: ipv4 ipv6 host + host: ^_^host^_^ + # OceanBase port + port: ^_^port^_^ + # database platform name + platform: mysql + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # database name + database: ^_^database^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: multiRow + # sql + sql: select tenant_id, tenant_name, info, status from oceanbase.__all_tenant; + # JDBC url + url: ^_^url^_^ + + - name: sql + priority: 2 + i18n: + zh-CN: Sql 信息 + en-US: Sql Info + ja-JP: Sql情報 + fields: + - field: con_id + type: 1 + label: true + i18n: + zh-CN: 租户id + en-US: TenantId + ja-JP: テナントID + - field: sql_select_count + type: 0 + i18n: + zh-CN: select 语句执行次数 + en-US: sql select count + ja-JP: sql select 回数 + - field: sql_insert_count + type: 0 + i18n: + zh-CN: insert 语句执行次数 + en-US: sql insert count + ja-JP: sql insert 回数 + - field: sql_update_count + type: 0 + i18n: + zh-CN: update 语句执行次数 + en-US: sql update count + ja-JP: sql update 回数 + - field: sql_delete_count + type: 0 + i18n: + zh-CN: delete 语句执行次数 + en-US: sql delete count + ja-JP: sql delete 回数 +# - field: com_commit +# type: 0 +# i18n: +# zh-CN: 事务提交次数 +# en-US: trans commit count +# ja-JP: trans commit 回数 +# - field: com_rollback +# type: 0 +# i18n: +# zh-CN: 事务回滚次数 +# en-US: trans rollback count +# ja-JP: trans rollback 回数 + aliasFields: + - con_id + - sql_select_count + - sql_insert_count + - sql_update_count + - sql_delete_count +# - com_commit +# - com_rollback + calculates: + - con_id=con_id + - sql_select_count=sql_select_count + - sql_insert_count=sql_insert_count + - sql_update_count=sql_update_count + - sql_delete_count=sql_delete_count + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: multiRow + sql: | + SELECT + con_id, + MAX(CASE WHEN name = 'sql select count' THEN value END) AS "sql_select_count", + MAX(CASE WHEN name = 'sql insert count' THEN value END) AS "sql_insert_count", + MAX(CASE WHEN name = 'sql update count' THEN value END) AS "sql_update_count", + MAX(CASE WHEN name = 'sql delete count' THEN value END) AS "sql_delete_count" + FROM + oceanbase.gv$sysstat + WHERE + CLASS = 8 + AND name IN ('sql select count', 'sql insert count', 'sql update count', 'sql delete count') + GROUP BY + con_id + ORDER BY + con_id; + url: ^_^url^_^ + + + - name: process_state + priority: 3 + i18n: + zh-CN: 进程状态 信息 + en-US: Process State Info + ja-JP: プロセス状態情報 + fields: + - field: state + type: 1 + label: true + i18n: + zh-CN: 进程状态 + en-US: State + ja-JP: 状態 + - field: num + type: 0 + i18n: + zh-CN: 该状态进程数量 + en-US: Num + ja-JP: プロセス総数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: multiRow + sql: select state, count(*) as num from information_schema.PROCESSLIST where state != '' group by state; + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-ollama.yml b/hertzbeat-manager/src/main/resources/define/app-ollama.yml new file mode 100644 index 0000000..dd818a2 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-ollama.yml @@ -0,0 +1,239 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: llm +# The monitoring type eg: linux windows tomcat mysql aws... +app: ollama +# The monitoring i18n name +name: + zh-CN: Ollama + en-US: Ollama + ja-JP: Ollama +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 Ollama 本地大模型服务的版本信息、已安装模型列表和运行中模型进行采集监控。<br>您可以点击"<i>新增 Ollama</i>"并进行配置,或者选择"<i>更多操作</i>",导入已有配置。 + en-US: HertzBeat monitors Ollama local LLM service including version info, installed models and running models.<br>You can click the "<i>New Ollama</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: HertzBeat 對 Ollama 本地大模型服務的版本資訊、已安裝模型列表和運行中模型進行採集監控。<br>您可以點擊"<i>新增 Ollama</i>"並進行配置,或者選擇"<i>更多操作</i>",導入已有配置。 + ja-JP: HertzBeat は Ollama ローカル LLM サービスのバージョン情報、インストール済みモデル一覧、実行中モデルを監視します。<br>「<i>新規 Ollama</i>」をクリックして設定できます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/ollama + en-US: https://hertzbeat.apache.org/docs/help/ollama +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + required: true + defaultValue: 11434 + - field: ssl + name: + zh-CN: SSL连接 + en-US: SSL + ja-JP: SSL利用 + type: boolean + required: false + - field: apiKey + name: + zh-CN: API Key + en-US: API Key + ja-JP: API キー + type: text + required: false +# collect metrics config list +metrics: + - name: version_info + i18n: + zh-CN: 版本信息 + en-US: Version Info + ja-JP: バージョン情報 + priority: 0 + fields: + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /api/version + method: GET + ssl: ^_^ssl^_^ + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Bearer Token + bearerTokenToken: ^_^apiKey^_^ + parseType: jsonPath + parseScript: '$' + + - name: models + i18n: + zh-CN: 已安装模型 + en-US: Installed Models + ja-JP: インストール済みモデル + priority: 1 + fields: + - field: name + type: 1 + label: true + i18n: + zh-CN: 模型名称 + en-US: Model Name + ja-JP: モデル名 + - field: model_size + type: 0 + unit: MB + i18n: + zh-CN: 模型大小 + en-US: Model Size + ja-JP: モデルサイズ + - field: parameter_size + type: 1 + i18n: + zh-CN: 参数规模 + en-US: Parameter Size + ja-JP: パラメータサイズ + - field: quantization_level + type: 1 + i18n: + zh-CN: 量化级别 + en-US: Quantization Level + ja-JP: 量子化レベル + - field: family + type: 1 + i18n: + zh-CN: 模型家族 + en-US: Model Family + ja-JP: モデルファミリー + - field: format + type: 1 + i18n: + zh-CN: 格式 + en-US: Format + ja-JP: フォーマット + - field: modified_at + type: 1 + i18n: + zh-CN: 修改时间 + en-US: Modified At + ja-JP: 更新日時 + aliasFields: + - name + - $.size + - $.details.parameter_size + - $.details.quantization_level + - $.details.family + - $.details.format + - modified_at + calculates: + - name = name + - model_size = $.size + - parameter_size = $.details.parameter_size + - quantization_level = $.details.quantization_level + - family = $.details.family + - format = $.details.format + - modified_at = modified_at + units: + - model_size=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /api/tags + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Bearer Token + bearerTokenToken: ^_^apiKey^_^ + parseType: jsonPath + parseScript: '$.models.*' + + - name: running_models + i18n: + zh-CN: 运行中模型 + en-US: Running Models + ja-JP: 実行中モデル + priority: 2 + fields: + - field: name + type: 1 + label: true + i18n: + zh-CN: 模型名称 + en-US: Model Name + ja-JP: モデル名 + - field: model_size + type: 0 + unit: MB + i18n: + zh-CN: 模型大小 + en-US: Model Size + ja-JP: モデルサイズ + - field: size_vram + type: 0 + unit: MB + i18n: + zh-CN: VRAM占用 + en-US: VRAM Size + ja-JP: VRAMサイズ + - field: expires_at + type: 1 + i18n: + zh-CN: 过期时间 + en-US: Expires At + ja-JP: 有効期限 + aliasFields: + - name + - $.size + - size_vram + - expires_at + calculates: + - name = name + - model_size = $.size + - size_vram = size_vram + - expires_at = expires_at + units: + - model_size=B->MB + - size_vram=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /api/ps + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Bearer Token + bearerTokenToken: ^_^apiKey^_^ + parseType: jsonPath + parseScript: '$.models.*' diff --git a/hertzbeat-manager/src/main/resources/define/app-openai.yml b/hertzbeat-manager/src/main/resources/define/app-openai.yml new file mode 100644 index 0000000..ccf52bc --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-openai.yml @@ -0,0 +1,365 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: llm +# The monitoring type eg: linux windows tomcat mysql aws... +app: openai +# The monitoring i18n name +name: + zh-CN: OpenAI 监控 + en-US: OpenAI + ja-JP: OpenAI +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 将调用 OpenAI API 接口,监测账户、账单、大模型 API 使用情况等数据。会话密钥获取请见使用指南。<br>您可以先点击“新增 OpenAI API”按钮并进行配置,或在“更多操作”中导入已有配置。 + en-US: HertzBeat will call the OpenAI API interface to monitor account, billing, and usage data of LLM API, etc. Please refer to the user guide for session key acquisition.<br>You can click the "Add OpenAI API" button to configure it first, or import existing configurations in the "More Actions" section. + zh-TW: HertzBeat 將調用 OpenAI API 接口,監測帳戶、帳單、大模型 API 使用情況等數據。請參閱使用指南以獲取會話密鑰。<br>您可以先點擊“新增 OpenAI API”按鈕並進行配置,或在“更多操作”中導入已有配置。 + ja-JP: Hertzbeatは、OpenAI APIを呼び出してメトリクスを監視します。<br>「<i>新規 OpenAI API</i>」をクリックして設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/openai + en-US: https://hertzbeat.apache.org/docs/help/openai +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: token + # name-param field display i18n name + name: + zh-CN: 会话密钥 + en-US: Session Key + ja-JP: セッションキー + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true +# collect metrics config list +metrics: + # metrics - cpu + - name: credit_grants + # metrics name i18n label + i18n: + zh-CN: 信用额度授予 + en-US: Credit Grants + ja-JP: 与信枠 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, i18n-metric name i18n label, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: total_granted + type: 0 + i18n: + zh-CN: 总授予额度 + en-US: Total Granted + ja-JP: 与信額合計 + unit: $ + - field: total_used + type: 0 + i18n: + zh-CN: 总使用额度 + en-US: Total Used + ja-JP: 使用した与信額 + unit: $ + - field: total_available + type: 0 + i18n: + zh-CN: 总可用额度 + en-US: Total Available + ja-JP: 使用可能な与信額 + unit: $ + - field: total_paid_available + type: 0 + i18n: + zh-CN: 总付费可用额度 + en-US: Total Paid Available + ja-JP: 使用可能な与信額合計 + unit: $ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: api.openai.com + # http port + port: 80 + # http url + url: /dashboard/billing/credit_grants + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: false + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Bearer Token + bearerTokenToken: ^_^token^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$' + + - name: model_cost + # metrics name i18n label + i18n: + zh-CN: 模型花费 + en-US: Model Cost + ja-JP: モデルコスト + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, i18n-metric name i18n label, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: name + type: 1 + i18n: + zh-CN: 模型名称 + en-US: Model Name + ja-JP: モデル名 + label: true + - field: cost + type: 0 + i18n: + zh-CN: 花费 + en-US: Cost + ja-JP: コスト + unit: $ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: api.openai.com + # http port + port: 80 + # http url + url: /dashboard/billing/usage + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: false + params: + exclude_project_costs: false + new_endpoint: true + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Bearer Token + bearerTokenToken: ^_^token^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$.daily_costs[0][line_items][*]' + + - name: billing_subscription + # metrics name i18n label + i18n: + zh-CN: 订阅计费 + en-US: Billing Subscription + ja-JP: 課金購読 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # collect metrics content + fields: + # field-metric name, i18n-metric name i18n label, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: has_payment_method + type: 1 + i18n: + zh-CN: 是否有支付方式 + en-US: Has Payment Method + ja-JP: 支払い方法の有無 + - field: canceled + type: 1 + i18n: + zh-CN: 订阅是否已取消 + en-US: Canceled + ja-JP: 購読がキャンセルされた + - field: canceled_at + type: 1 + i18n: + zh-CN: 订阅取消时间 + en-US: Canceled At + ja-JP: 購読キャンセル時間 + - field: delinquent + type: 1 + i18n: + zh-CN: 订阅是否拖欠 + en-US: Delinquent + ja-JP: 購読料滞納の有無 + - field: soft_limit + type: 0 + i18n: + zh-CN: 一定时间内最大使用量限制 + en-US: Soft Limit + ja-JP: 一定期間の最大使用量制限 + - field: hard_limit + type: 0 + i18n: + zh-CN: 最大使用量限制 + en-US: Hard Limit + ja-JP: 最大使用量制限 + - field: system_hard_limit + type: 0 + i18n: + zh-CN: 系统硬限制使用量 + en-US: System Hard Limit + ja-JP: システムの最大使用量制限 + - field: soft_limit_usd + type: 0 + i18n: + zh-CN: 软限制费用 + en-US: Soft Limit USD + ja-JP: 一定期間の最大使用量制限の費用 + unit: $ + - field: hard_limit_usd + type: 0 + i18n: + zh-CN: 硬限制费用 + en-US: Hard Limit USD + ja-JP: 最大使用量制限の費用 + unit: $ + - field: system_hard_limit_usd + type: 0 + i18n: + zh-CN: 系统硬限制费用 + en-US: System Hard Limit USD + ja-JP: システムの最大使用量制限の費用 + unit: $ + - field: plan + type: 1 + i18n: + zh-CN: 订阅计划 + en-US: Plan + ja-JP: 購読プラン + - field: primary + type: 1 + i18n: + zh-CN: 是否是主要订阅 + en-US: Primary + ja-JP: プライマリー購読 + unit: $ + - field: billing_mechanism + type: 1 + i18n: + zh-CN: 结算机制 + en-US: Billing Mechanism + ja-JP: 課金仕組み + - field: is_arrears_eligible + type: 1 + i18n: + zh-CN: 是否符合拖欠资格 + en-US: Is Arrears Eligible + ja-JP: 延滞の対象 + - field: max_balance + type: 0 + i18n: + zh-CN: 最大余额 + en-US: Max Balance + ja-JP: 最大バランス + unit: $ + - field: auto_recharge_eligible + type: 1 + i18n: + zh-CN: 是否符合自动充值资格 + en-US: Auto Recharge Eligible + ja-JP: オートリチャージ対象 + - field: auto_recharge_enabled + type: 1 + i18n: + zh-CN: 是否启用自动充值 + en-US: Auto Recharge Enabled + ja-JP: オートリチャージ有効 + - field: auto_recharge_threshold + type: 0 + i18n: + zh-CN: 自动充值阈值 + en-US: Auto Recharge Threshold + ja-JP: オートリチャージしきい値 + unit: $ + - field: auto_recharge_to_balance + type: 0 + i18n: + zh-CN: 自动充值金额 + en-US: Auto Recharge To Balance + ja-JP: オートリチャージ金額 + unit: $ + - field: trust_tier + type: 1 + i18n: + zh-CN: 信用等级 + en-US: Trust Tier + ja-JP: 信託層 + - field: account_name + type: 1 + i18n: + zh-CN: 账户名称 + en-US: Account Name + ja-JP: アカウント名 + - field: po_number + type: 1 + i18n: + zh-CN: 采购号 + en-US: Po Number + ja-JP: 購入番号 + - field: billing_email + type: 1 + i18n: + zh-CN: 账单邮箱 + en-US: Billing Email + ja-JP: 請求先メール + - field: tax_ids + type: 1 + i18n: + zh-CN: 税务ID + en-US: Tax IDs + ja-JP: 納税者番号 + - field: billing_address + type: 1 + i18n: + zh-CN: 结算地址 + en-US: Billing Address + ja-JP: 請求先住所 + - field: business_address + type: 1 + i18n: + zh-CN: 业务地址 + en-US: Business Address + ja-JP: ビジネスアドレス + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: api.openai.com + # http port + port: 80 + # http url + url: /dashboard/billing/subscription + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: false + params: + start_date: ${@date} + end_date: ${@date+1d} + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Bearer Token + bearerTokenToken: ^_^token^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$' + diff --git a/hertzbeat-manager/src/main/resources/define/app-opengauss.yml b/hertzbeat-manager/src/main/resources/define/app-opengauss.yml new file mode 100644 index 0000000..407e0ec --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-opengauss.yml @@ -0,0 +1,260 @@ +# 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. + +category: db +app: opengauss +name: + zh-CN: OpenGauss数据库 + en-US: OpenGauss DB + ja-JP: OpenGauss データベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 OpenGauss 数据库的通用性能指标 (basic、state、activity etc) 进行采集监控,支持版本为 PostgreSQL10+。<br>您可以点击“<i>新建 OpenGauss 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC Protocol</a> to configure SQL for collecting general metrics of OpenGauss database (basic、state、activity etc). Supported version is PostgreSQL10+. <br>You can click "<i>New OpenGauss Database</i>" and configure it, or select "<i>More Action</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 協議</a> 通過配置 SQL 對 OpenGauss 數據庫的通用性能指標 (basic、state、activity etc)進行采集監控,支持版本爲 PostgreSQL10+。<br>您可以點擊“<i>新建 OpenGauss 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBCプロトコルを介して</a> OpenGauss データベースの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 OpenGauss データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/opengauss + en-US: https://hertzbeat.apache.org/docs/help/opengauss +params: + - field: host + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + required: true + defaultValue: 5432 + - field: timeout + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + type: number + # when type is number, range is required + range: '[400,200000]' + required: false + hide: true + defaultValue: 6000 + - field: database + name: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + type: text + defaultValue: postgres + required: false + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + - field: url + name: + zh-CN: URL + en-US: URL + ja-JP: URL + type: text + required: false + hide: true +metrics: + - name: basic + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + i18n: + zh-CN: 基本信息 + en-US: Basic Info + ja-JP: 基礎情報 + priority: 0 + # monitor metrics list + fields: + # metric information includes field name, type field type: 0-number, 1-string, label-whether it is a metric label field, unit-metric unit + - field: server_version + i18n: + zh-CN: 数据库版本 + en-US: Server Version + ja-JP: バージョン + type: 1 + label: true + - field: port + i18n: + zh-CN: 端口号 + en-US: Port + ja-JP: ポート + type: 1 + - field: server_encoding + i18n: + zh-CN: 数据库编码格式 + en-US: Encoding + ja-JP: エンコード + type: 1 + - field: data_directory + i18n: + zh-CN: 数据存储目录 + en-US: Data Directory + ja-JP: データディレクトリ + type: 1 + - field: max_connections + i18n: + zh-CN: 最大连接数 + en-US: Max Connection + ja-JP: 最大接続数 + type: 0 + unit: sbc + protocol: jdbc + jdbc: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + # SQL query method: oneRow, multiRow, columns + queryType: columns + # sql + sql: select name, setting as value from pg_settings where name = 'max_connections' or name = 'server_version' or name = 'server_encoding' or name = 'port' or name = 'data_directory'; + url: ^_^url^_^ + + - name: state + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: 状態情報 + priority: 1 + fields: + # metric information includes field name, type field type: 0-number, 1-string, label-whether it is a metric label field, unit-metric unit + - field: name + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + type: 1 + - field: conflicts + i18n: + zh-CN: 冲突次数 + en-US: Conflicts + ja-JP: コンフリクト回数 + type: 0 + unit: times + - field: deadlocks + i18n: + zh-CN: 死锁次数 + en-US: Deadlocks + ja-JP: デッドロック回数 + type: 0 + unit: times + - field: blks_read + i18n: + zh-CN: 块读取数 + en-US: Blocks Read + ja-JP: 読み取られたブロック + type: 0 + unit: blocks per second + - field: blks_hit + i18n: + zh-CN: 块命中数 + en-US: Blocks Hit + ja-JP: ヒットブロック + type: 0 + unit: blocks per second + - field: blk_read_time + i18n: + zh-CN: 块读取时间 + en-US: Block Read Time + ja-JP: 読み取られタイム + type: 0 + unit: ms + - field: blk_write_time + i18n: + zh-CN: 块写入时间 + en-US: Block Write + ja-JP: 書き込まれ時間 + type: 0 + unit: ms + - field: stats_reset + i18n: + zh-CN: 统计信息重置时间 + en-US: States Reset + ja-JP: 統計リセット + type: 1 + protocol: jdbc + jdbc: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + # SQL query method: oneRow, multiRow, columns + queryType: multiRow + # sql + sql: SELECT COALESCE(datname,'shared-object') as name, conflicts, deadlocks, blks_read, blks_hit, blk_read_time, blk_write_time, stats_reset from pg_stat_database where (datname != 'template1' and datname != 'template0') or datname is null; + url: ^_^url^_^ + + - name: activity + i18n: + zh-CN: 活跃进程 + en-US: Activity + ja-JP: 活動情報 + priority: 2 + fields: + # metric information includes field name, type field type: 0-number, 1-string, label-whether it is a metric label field, unit-metric unit + - field: running + i18n: + zh-CN: 运行中进程数 + en-US: Running + ja-JP: 実行中 + type: 0 + unit: sbc + protocol: jdbc + jdbc: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + # SQL query method: oneRow, multiRow, columns + queryType: oneRow + # sql + sql: SELECT count(*) as running FROM pg_stat_activity WHERE NOT pid=pg_backend_pid(); + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-opensuse.yml b/hertzbeat-manager/src/main/resources/define/app-opensuse.yml new file mode 100644 index 0000000..e694ae4 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-opensuse.yml @@ -0,0 +1,764 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: opensuse +# The monitoring i18n name +name: + zh-CN: OpenSUSE + en-US: OpenSUSE + ja-JP: OpenSUSE +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 OpenSUSE 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 OpenSUSE</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors OpenSUSE operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New OpenSUSE</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 OpenSUSE 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 OpenSUSE</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> OpenSUSEシステムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 OpenSUSE</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/opensuse + en-US: https://hertzbeat.apache.org/docs/help/opensuse +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: (uname -r ; hostname ; uptime | awk -F "," '{print $1}' | sed "s/ //g") | sed ":a;N;s/\n/^/g;ta" | awk -F '^' 'BEGIN{print "version hostname uptime"} {print $1, $2, $3}' + # ssh response data parse type: oneRow, multiRow + parseType: multiRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100-idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: "LANG=C lscpu | awk -F: '$1==\"Model name\" {print $2}';awk '/processor/{core++} END{print core}' /proc/cpuinfo;uptime | sed 's/,/ /g' | awk '{for(i=NF-2;i<=NF;i++)print $i }' | xargs;vmstat 1 1 | awk 'NR==3{print $11}';vmstat 1 1 | awk 'NR==3{print $12}';vmstat 1 2 | awk 'NR==4{print $15}'" + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: free -m | awk 'BEGIN{print "total used free buff_cache available"} NR==2{print $2,$3,$4,$6,$7}' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + en-US: Partition Num + ja-JP: パーティション + - field: block_write + type: 0 + i18n: + zh-CN: 写磁盘块数 + en-US: Block Write + ja-JP: 書き込みディスクブロック数 + - field: block_read + type: 0 + i18n: + zh-CN: 读磁盘块数 + en-US: Block Read + ja-JP: 読み取りブロック数 + - field: write_rate + type: 0 + unit: iops + i18n: + zh-CN: 磁盘写速率 + en-US: Write Rate + ja-JP: ディスク書き込み速度 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: vmstat -D | awk 'NR==1{print $1}';vmstat -D | awk 'NR==2{print $1}';vmstat 1 1 | awk 'NR==3{print $10}';vmstat 1 1 | awk 'NR==3{print $9}';vmstat 1 1 | awk 'NR==3{print $16}' + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: cat /proc/net/dev | tail -n +3 | awk 'BEGIN{ print "interface_name receive_bytes transmit_bytes"} {print $1,$2,$10}' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -m | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-oracle.yml b/hertzbeat-manager/src/main/resources/define/app-oracle.yml new file mode 100644 index 0000000..a6dfdfa --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-oracle.yml @@ -0,0 +1,900 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: oracle +# The monitoring i18n name +name: + zh-CN: Oracle数据库 + en-US: Oracle DB + ja-JP: Oracle データベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 Oracle 数据库的通用性能指标(表空间、性能状态、用户连接、缓存、慢SQL等)进行采集监控。<br>您可以点击“<i>新建 Oracle 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC Protocol</a> to configure SQL for collecting general metrics of Oracle database (tablespace、user connect、performance、slow SQL etc.). <br>You can click "<i>New Oracle Database</i>" and configure it, or select "<i>More Operations</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 協議</a> 通過配置 SQL 對 Oracle 數據庫的通用性能指標(表空間、性能狀態、用戶連接、緩存、慢SQL等)進行采集監控。<br>您可以點擊“<i>新建 Oracle 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBCプロトコルを介して</a> Oracle データベースの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Oracle データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/oracle/ + en-US: https://hertzbeat.apache.org/docs/help/oracle/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 1521 + - field: timeout + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + type: number + range: '[400,200000]' + required: false + hide: true + defaultValue: 6000 + - field: reuseConnection + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + type: boolean + required: false + defaultValue: true + - field: database + name: + zh-CN: 服务名 + en-US: Service Name + ja-JP: サービス名 + type: text + required: false + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + - field: url + name: + zh-CN: URL + en-US: URL + ja-JP: URL + type: text + required: false + hide: true + +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 基本信息 + en-US: Basic Info + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: database_version + type: 1 + label: true + i18n: + zh-CN: 数据库版本 + en-US: Database Version + ja-JP: データベースバージョン + - field: hostname + type: 1 + i18n: + zh-CN: 主机名 + en-US: Host Name + ja-JP: ホスト名 + - field: instance_name + type: 1 + i18n: + zh-CN: 实例名 + en-US: Instance Name + ja-JP: インスタンス名 + - field: startup_time + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Startup Time + ja-JP: 起動時間 + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - VERSION + - HOST_NAME + - INSTANCE_NAME + - STARTUP_TIME + - STATUS + # (optional)mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - database_version=VERSION + - hostname=HOST_NAME + - instance_name=INSTANCE_NAME + - startup_time=STARTUP_TIME + - status=STATUS + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # host: ipv4 ipv6 host + host: ^_^host^_^ + port: ^_^port^_^ + # database platform name + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: oneRow + # sql + sql: "select * from sys.v_$instance" + # JDBC url + url: ^_^url^_^ + + - name: tablespace + i18n: + zh-CN: 表空间 + en-US: Tablespace + ja-JP: 表領域 + priority: 1 + fields: + - field: file_id + type: 1 + label: true + i18n: + zh-CN: 文件ID + en-US: File ID + ja-JP: ファイルID + - field: file_name + type: 1 + i18n: + zh-CN: 文件名 + en-US: File Name + ja-JP: ファイル名 + - field: tablespace_name + type: 1 + i18n: + zh-CN: 表空间名 + en-US: Tablespace Name + ja-JP: 表領域名 + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + - field: bytes + type: 0 + i18n: + zh-CN: 字节数 + en-US: Bytes + ja-JP: バイト数 + unit: MB + - field: blocks + type: 0 + i18n: + zh-CN: 块数 + en-US: Blocks + ja-JP: ブロック数 + unit: 块 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: "select file_id, file_name, tablespace_name, status, bytes / 1024 / 1024 as bytes, blocks from dba_data_files" + url: ^_^url^_^ + + - name: total_sessions + i18n: + zh-CN: 会话总数 + en-US: Total Sessions + ja-JP: セッション総数 + priority: 2 + fields: + - field: count + type: 0 + i18n: + zh-CN: 总数 + en-US: Count + ja-JP: 総数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: oneRow + sql: "select count(*) as count from v$session" + url: ^_^url^_^ + + - name: active_sessions + i18n: + zh-CN: 活动会话 + en-US: Active Sessions + ja-JP: アクティブなセッション + priority: 3 + fields: + - field: count + type: 0 + i18n: + zh-CN: 总数 + en-US: Count + ja-JP: 総数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: oneRow + sql: "select count(*) as count from v$session where username is not null and status = 'ACTIVE'" + url: ^_^url^_^ + + - name: background_sessions + i18n: + zh-CN: 后台会话 + en-US: Background Sessions + ja-JP: バックグラウンドセッション + priority: 4 + fields: + - field: count + type: 0 + i18n: + zh-CN: 总数 + en-US: Count + ja-JP: 総数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: oneRow + sql: "select count(*) as count from v$session where username is null" + url: ^_^url^_^ + + - name: connection + i18n: + zh-CN: 连接 + en-US: Connection + ja-JP: 接続 + priority: 5 + fields: + - field: username + type: 1 + i18n: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + label: true + - field: count + i18n: + zh-CN: 总数 + en-US: Count + ja-JP: 総数 + type: 0 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: oneRow + sql: "SELECT username, count( username ) as count FROM v$session WHERE username IS NOT NULL GROUP BY username" + url: ^_^url^_^ + + - name: performance + i18n: + zh-CN: 性能 + en-US: Performance + ja-JP: パフォーマンス + priority: 6 + fields: + - field: qps + type: 0 + i18n: + zh-CN: 每秒查询数 + en-US: QPS + ja-JP: QPS + unit: qps + - field: tps + type: 0 + i18n: + zh-CN: 每秒事务数 + en-US: TPS + ja-JP: TPS + unit: tps + - field: mbps + type: 0 + i18n: + zh-CN: 每秒IO读写 + en-US: MBPS + ja-JP: MBPS + unit: mbps + aliasFields: + - I/O Requests per Second + - User Transaction Per Sec + - I/O Megabytes per Second + calculates: + - qps=I/O Requests per Second + - tps=User Transaction Per Sec + - mbps=I/O Megabytes per Second + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: columns + sql: "select metric_name, value from gv$sysmetric where metric_name = 'I/O Megabytes per Second' or metric_name = 'User Transaction Per Sec' or metric_name = 'I/O Requests per Second'" + url: ^_^url^_^ + + + - name: percentage + i18n: + zh-CN: 表空间百分比 + en-US: Percentage + ja-JP: パーセント + priority: 7 + fields: + - field: tablespace_name + type: 1 + i18n: + zh-CN: 表空间名 + en-US: Tablespace Name + ja-JP: 表領域名 + label: true + - field: total + i18n: + zh-CN: 全部 + en-US: Total + ja-JP: 合計 + type: 0 + - field: used + i18n: + zh-CN: 已用 + en-US: Used + ja-JP: 使用済み + type: 0 + - field: free + i18n: + zh-CN: 空闲 + en-US: Free + ja-JP: 利用可能 + type: 0 + - field: used_percentage + type: 0 + i18n: + zh-CN: 已用百分比 + en-US: Used Percentage + ja-JP: 使用した比率 + unit: '%' + - field: free_percentage + type: 0 + i18n: + zh-CN: 空闲百分比 + en-US: Free Percentage + ja-JP: 利用可能な比率 + unit: '%' + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: "SELECT tablespace_name,ROUND ( (TABLESPACE_SIZE * 8 / 1024), 0) AS total,ROUND ( (USED_SPACE * 8 / 1024), 0) AS used,ROUND ( ( (TABLESPACE_SIZE * 8 / 1024) - (USED_SPACE * 8 / 1024)), 0) AS free,ROUND ( (USED_PERCENT), 0) AS used_percentage,100 - ROUND ( (USED_PERCENT), 0) AS free_percentage FROM sys.dba_tablespace_usage_metrics ORDER BY used_percent DESC" + url: ^_^url^_^ + + - name: process + i18n: + zh-CN: 进程 + en-US: Process + ja-JP: プロセス + priority: 8 + fields: + - field: process_count + type: 0 + i18n: + zh-CN: 进程数 + en-US: Process Count + ja-JP: プロセス数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: oneRow + sql: "select count(*) as process_count from v$process" + url: ^_^url^_^ + + - name: transaction + i18n: + zh-CN: 事务 + en-US: Transaction + ja-JP: トランザクション + priority: 9 + fields: + - field: commits + type: 0 + unit: 't/s' + i18n: + zh-CN: 提交数 + en-US: Commits + ja-JP: コミット回数 + - field: rollbacks + type: 0 + unit: 't/s' + i18n: + zh-CN: 回滚数 + en-US: Rollbacks + ja-JP: ロールバック回数 + aliasFields: + - User Commits Per Sec + - User Rollbacks Per Sec + calculates: + - commits=User Commits Per Sec + - rollbacks=User Rollbacks Per Sec + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: columns + sql: "select metric_name, value from gv$sysmetric where metric_name = 'User Commits Per Sec' or metric_name = 'User Rollbacks Per Sec'" + url: ^_^url^_^ + + - name: wait + i18n: + zh-CN: 等待 + en-US: Wait + ja-JP: 待機 + priority: 10 + fields: + - field: concurrent_wait_time + type: 0 + unit: ms + i18n: + zh-CN: 并发等待时间 + en-US: Concurrent Wait Time + ja-JP: 同時実行の待機時間 + - field: commit_wait_time + type: 0 + unit: ms + i18n: + zh-CN: 提交等待时间 + en-US: Commit Wait Time + ja-JP: コミット待機時間 + - field: app_wait_time + type: 0 + unit: ms + i18n: + zh-CN: 应用等待时间 + en-US: Application Wait Time + ja-JP: 応用待機時間 + - field: network_wait_time + type: 0 + unit: ms + i18n: + zh-CN: 网络等待时间 + en-US: Network Wait Time + ja-JP: ネットワーク待機時間 + - field: system_io_wait_time + type: 0 + unit: ms + i18n: + zh-CN: 系统I/O等待时间 + en-US: System I/O Wait Time + ja-JP: システムI/O待機時間 + - field: user_io_wait_time + type: 0 + unit: ms + i18n: + zh-CN: 用户I/O等待时间 + en-US: User I/O Wait Time + ja-JP: ユーザーI/O待機時間 + - field: configure_wait_time + type: 0 + unit: ms + i18n: + zh-CN: 配置等待时间 + en-US: Configure Wait Time + ja-JP: 配置待機時間 + - field: scheduler_wait_time + type: 0 + unit: ms + i18n: + zh-CN: 调度等待时间 + en-US: Scheduler Wait Time + ja-JP: Scheduler待機時間 + aliasFields: + - System I/O + - Application + - User I/O + - Commit + - Concurrency + - Configuration + - Idle + - Other + calculates: + - system_io_wait_time=System I/O + - app_wait_time=Application + - user_io_wait_time=User I/O + - commit_wait_time=Commit + - concurrent_wait_time=Concurrency + - configure_wait_time=Configuration + - scheduler_wait_time=Idle + - network_wait_time=Other + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: columns + sql: "select wait_class, sum(time_waited) total_wait_time from v$active_session_history where session_state = 'WAITING' GROUP BY wait_class ORDER BY total_wait_time DESC" + url: ^_^url^_^ + + - name: cpu_stats + i18n: + zh-CN: CPU状态 + en-US: CPU Stats + ja-JP: CPU状態 + priority: 11 + fields: + - field: type + type: 1 + label: true + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: タイプ + - field: num + type: 1 + i18n: + zh-CN: 数量 + en-US: Num + ja-JP: 数量 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: "select stat_name as type, value as num from v$osstat where stat_name like '%CPU%' or stat_name like '%TIME'" + url: ^_^url^_^ + + - name: mem_stats + i18n: + zh-CN: 内存状态 + en-US: Memory Stats + ja-JP: メモリ状態 + priority: 12 + fields: + - field: type + type: 1 + label: true + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: タイプ + - field: num + type: 1 + i18n: + zh-CN: 数量 + en-US: Num + ja-JP: 数量 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: "select stat_name as type, value as num from v$osstat where stat_name like '%BYTES'" + url: ^_^url^_^ + + - name: cache_hit_ratio + i18n: + zh-CN: 缓存命中率 + en-US: Cache Hit Ratio + ja-JP: キャッシュ命中率 + priority: 13 + fields: + - field: lib_cache_hit_ratio + type: 0 + i18n: + zh-CN: 库缓存命中率 + en-US: LIB CACHE HIT RATIO + ja-JP: LIBキャッシュ命中率 + - field: buffer_cache_hit_ratio + type: 0 + i18n: + zh-CN: 缓冲区缓存命中率 + en-US: BUFFER CACHE HIT RATIO + ja-JP: バッファキャッシュ命中率 + aliasFields: + - Library Cache Hit Ratio + - Buffer Cache Hit Ratio + calculates: + - lib_cache_hit_ratio=Library Cache Hit Ratio + - buffer_cache_hit_ratio=Buffer Cache Hit Ratio + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: columns + sql: "select metric_name, value from gv$sysmetric where metric_name like '%Cache Hit Ratio' order by end_time asc" + url: ^_^url^_^ + + - name: slow_query + i18n: + zh-CN: 慢查询 + en-US: Slow Query + ja-JP: スロークエリ + priority: 14 + fields: + - field: sql_id + type: 1 + i18n: + zh-CN: sql 主键 + en-US: SQL ID + ja-JP: SQL ID + - field: child_number + type: 1 + i18n: + zh-CN: 子编号 + en-US: Child Number + ja-JP: 子カーソルの番号 + - field: executions + type: 1 + i18n: + zh-CN: 执行数 + en-US: EXECUTIONS + ja-JP: 行われた実行数 + unit: 次 + - field: per_secs + type: 1 + i18n: + zh-CN: 每秒执行数 + en-US: Per Secs + ja-JP: 1秒当たりの実行数 + unit: 秒 + - field: cpu_secs + type: 1 + i18n: + zh-CN: 每秒CPU + en-US: CPU Secs + ja-JP: 使用したCPU時間 + unit: 秒 + - field: buffer_gets + type: 1 + i18n: + zh-CN: 获得的缓冲区 + en-US: Buffer Gets + ja-JP: バッファ取得数 + - field: disk_reads + type: 1 + i18n: + zh-CN: 磁盘读取 + en-US: Disk Reads + ja-JP: ディスク読取り数 + - field: fetches + type: 1 + i18n: + zh-CN: 获取数量 + en-US: Fetches + ja-JP: SQL文に関連するフェッチ数 + - field: parse_calls + type: 1 + i18n: + zh-CN: 解析调用 + en-US: Parse Calls + ja-JP: 解析コール数 + - field: optimizer_cost + type: 1 + i18n: + zh-CN: 优化器成本 + en-US: Optimizer Cost + ja-JP: オプティマイザによるこの問合せのコスト + - field: sql_text + type: 1 + i18n: + zh-CN: SQL文本 + en-US: SQL Text + ja-JP: SQLテキスト + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: "SELECT * FROM (SELECT sql_id, child_number, executions, ROUND(CASE WHEN executions = 0 THEN NULL ELSE elapsed_time / (executions*1000000) END,4) AS per_secs, cpu_time / 1000000 AS cpu_secs, buffer_gets, disk_reads, fetches, parse_calls, optimizer_cost, sql_text FROM v$sql ) where rownum <= 10 ORDER BY per_secs DESC" + url: ^_^url^_^ + + - name: users + i18n: + zh-CN: 用户信息 + en-US: Users + ja-JP: スロークエリ + priority: 15 + fields: + - field: username + type: 1 + i18n: + zh-CN: 用户名 + en-US: User Name + ja-JP: ユーザー名 + - field: account_status + type: 1 + i18n: + zh-CN: 账号状态 + en-US: Account Status + ja-JP: アカウントのステータス + - field: lock_date + type: 1 + i18n: + zh-CN: 锁定时间 + en-US: Lock Date + ja-JP: ロック日付 + - field: expiry_date + type: 1 + i18n: + zh-CN: 密码失效时间 + en-US: Password Expiry Date + ja-JP: パスワードの有効期限 + - field: expiry_seconds + type: 0 + i18n: + zh-CN: 密码剩余有效时间 + en-US: Password Validity Period Remaining + ja-JP: パスワードの有効期間残り + unit: 秒 + - field: created + type: 1 + i18n: + zh-CN: 创建时间 + en-US: Creation Date + ja-JP: 作成日 + - field: authentication_type + type: 1 + i18n: + zh-CN: 认证类型 + en-US: Authentication Type + ja-JP: 認証タイプ + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: oracle + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: "SELECT username,account_status,lock_date,expiry_date,ceil((expiry_date-sysdate)* 24 * 60 * 60) as expiry_seconds,created,authentication_type FROM DBA_USERS ORDER BY expiry_date ASC" + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-ping.yml b/hertzbeat-manager/src/main/resources/define/app-ping.yml new file mode 100644 index 0000000..f153f67 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-ping.yml @@ -0,0 +1,94 @@ +# 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. + +# Category of this monitoring type: +# service - application service monitoring +# db - database monitoring +# custom - custom monitoring +# os - operating system monitoring +category: service +# Monitoring application type name (consistent with file name) eg: linux windows tomcat mysql aws... +app: ping +# The app api i18n name +name: + zh-CN: PING连通性 + en-US: PING CONNECT + ja-JP: PING接続 +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 以 ms 为指标单位,对端 HOST 地址进行 PING 操作,判断其连通性。您可以点击“<i>新建 PING 连通性</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses 'ms' as the metric unit to PING the end HOST address and determines the connectivity. You could click the "Add API " button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: HertzBeat 以 ms 爲指標單位,對端 HOST 地址進行 PING 操作,判斷其連通性。您可以點擊“<i>新建 PING 連通性</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: HertzBeat は、接続性を判断するために、エンド HOST アドレスに対して PING 操作を ms 単位で実行します。<br>「<i>新規 PING接続</i>」をクリックして設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/ping + en-US: https://hertzbeat.apache.org/docs/help/ping +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: Ping超时时间(ms) + en-US: Ping Timeout(ms) + ja-JP: Pingタイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 +# collect metrics config list +metrics: + # First monitoring metric group summary + # Note: The built-in monitoring metrics include (responseTime - response time) + - name: summary + i18n: + zh-CN: 概要 + en-US: Summary + ja-JP: 概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # Monitoring and collection protocols eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: icmp + # Specific collection configuration when the protocol is the ping protocol + icmp: + # icmp host + host: ^_^host^_^ + # timeout + timeout: ^_^timeout^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-pop3.yml b/hertzbeat-manager/src/main/resources/define/app-pop3.yml new file mode 100644 index 0000000..2d96ba4 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-pop3.yml @@ -0,0 +1,185 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: service +# Monitoring application type name (consistent with file name) eg: linux windows tomcat mysql aws... +app: pop3 +# The app api i18n name +name: + zh-CN: POP3邮件服务器 + en-US: POP3 Email Server + ja-JP: POP3メールサーバー +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://www.alibabacloud.com/help/zh/alibaba-mail/latest/how-to-turn-off-or-open-the-pop3-or-imap-protocol-for-accounts'> POP3 </a> 通过开启邮箱 pop3 服务 对 POP3服务器 的通用性能指标(email_count、mailbox_size)进行采集监控。<br>您可以点击“<i>新建 POP3</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://www.alibabacloud.com/help/zh/alibaba-mail/latest/how-to-turn-off-or-open-the-pop3-or-imap-protocol-for-accounts'> POP3 </a> to open pop3 service for collecting general metrics of POP3 Server (email_count, mailbox_size.). <br>You can click "<i>New POP3</i>" and configure it, or select "<i>More Operations</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://www.alibabacloud.com/help/zh/alibaba-mail/latest/how-to-turn-off-or-open-the-pop3-or-imap-protocol-for-accounts'> POP3 </a> 通過开启邮箱 pop3 服务 對 POP3服务器 的通用性能指標(邮件数量、邮箱大小等)進行采集監控。<br>您可以點擊“<i>新建 POP3</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: HertzBeat は <a class='help_module_content' href='https://www.alibabacloud.com/help/zh/alibaba-mail/latest/how-to-turn-off-or-open-the-pop3-or-imap-protocol-for-accounts'>POP3 サービスを介して</a>POP3メールサーバーの一般的なメトリクスを監視します。<br>「<i>新規 POP3メールサーバー</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/pop3 + en-US: https://hertzbeat.apache.org/docs/help/pop3 +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 110 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL + # When the type is boolean, the frontend will display a switch for it. + type: boolean + # required-true or false + required: false + # field-param field key + - field: email + # name-param field display i18n name + name: + zh-CN: POP邮箱地址 + en-US: POP Address + ja-JP: POPアドレス + # type-param field type(most mapping the html input type) The type "text" belongs to a text input field. + type: text + # required-true or false + required: true + # field-param field key + - field: authorize + # name-param field display i18n name + name: + zh-CN: 授权码 + en-US: Authorize Code + ja-JP: 認証コード + # type-param field type(most mapping the html input type) The type "text" belongs to a text input field. + type: text + # required-true or false + required: true + +# collect metrics config list +metrics: + # metrics - available + - name: available + i18n: + zh-CN: 可用性 + en-US: Available + ja-JP: 可用性 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: pop3 + # Specific collection configuration when protocol is http protocol + pop3: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # password + authorize: ^_^authorize^_^ + + - name: email_status + i18n: + zh-CN: 邮箱状态信息 + en-US: Email Status + ja-JP: 電子メールステータス情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + fields: + - field: email_count + type: 0 + i18n: + zh-CN: 邮箱数量 + en-US: Email Count + ja-JP: 電子メール数量 + - field: mailbox_size + type: 0 + unit: KB + i18n: + zh-CN: 邮箱大小 + en-US: MailBox Size + ja-JP: メールボックスサイズ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: pop3 + # Specific collection configuration when protocol is http protocol + pop3: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # password + authorize: ^_^authorize^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-port.yml b/hertzbeat-manager/src/main/resources/define/app-port.yml new file mode 100644 index 0000000..55bae3e --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-port.yml @@ -0,0 +1,106 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: port +# The app api i18n name +name: + zh-CN: 端口可用性 + en-US: PORT TELNET + ja-JP: ポート可用性 +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 通过判断对端服务中暴露的TCP端口是否可用,进而判断对端服务是否可用。同时以ms为指标,对响应时间等指标进行监测。您可以点击“<i>新建 端口可用性</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat determines whether the exposed tcp ports in the peer service are available, thereby determining whether the peer service is available. Simultaneously monitoring response time and other metrics using ms as metric unit. You could click the "<i>New port</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: HertzBeat 通過判斷對端服務中暴露的TCP端口是否可用,進而判斷對端服務是否可用。同時以ms爲指標,對響應時間等指標進行監測。您可以點擊“<i>新建 端口可用性</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: HertzBeat は、接続性を判断するために、エンド TCP ポートに対してリクエストを ms 単位で実行します。<br>「<i>新規 ポート可用性</i>」をクリックして設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/port + en-US: https://hertzbeat.apache.org/docs/help/port +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value 80 + defaultValue: 80 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - summary + - name: summary + i18n: + zh-CN: 概要 + en-US: Summary + ja-JP: 概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: telnet + # Specific collection configuration when protocol is telnet protocol + telnet: + # telnet host + host: ^_^host^_^ + # port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-postgresql.yml b/hertzbeat-manager/src/main/resources/define/app-postgresql.yml new file mode 100644 index 0000000..1af48ba --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-postgresql.yml @@ -0,0 +1,1174 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: postgresql +# The monitoring i18n name +name: + zh-CN: PostgreSQL数据库 + en-US: PostgreSQL DB + ja-JP: PostgreSQL データベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 PostgreSQL 数据库的通用性能指标 (basic、state、activity etc) 进行采集监控,支持版本为 PostgreSQL10+。<br>您可以点击“<i>新建 PostgreSQL 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC Protocol</a> to configure SQL for collecting general metrics of PostgreSQL database (basic、state、activity etc). Supported version is PostgreSQL10+. <br>You can click "<i>New PostgreSQL Database</i>" and configure it, or select "<i>More Action</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 協議</a> 通過配置 SQL 對 PostgreSQL 數據庫的通用性能指標 (basic、state、activity etc)進行采集監控,支持版本爲 PostgreSQL10+。<br>您可以點擊“<i>新建 PostgreSQL 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBCプロトコルを介して</a> PostgreSQL データベース(PostgreSQL10+)の一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 PostgreSQL データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/postgresql + en-US: https://hertzbeat.apache.org/docs/help/postgresql +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 5432 + - field: timeout + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + type: number + range: '[400,200000]' + required: false + hide: true + defaultValue: 6000 + - field: database + name: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + type: text + defaultValue: postgres + required: false + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + - field: url + name: + zh-CN: URL + en-US: URL + ja-JP: URL + type: text + required: false + hide: true + - field: enableSshTunnel + name: + zh-CN: 是否启用SSH隧道 + en-US: Enable SSH Tunnel + ja-JP: SSHトンネルの有効化 + type: boolean + required: true + hide: true + - field: sshHost + name: + zh-CN: SSH Host + en-US: SSH Host + ja-JP: SSHホスト + type: text + required: false + placeholder: 'When Enable SSH Tunnel' + hide: true + - field: sshPort + name: + zh-CN: SSH端口 + en-US: SSH Port + ja-JP: SSHポート + type: number + range: '[0,65535]' + required: false + defaultValue: 22 + placeholder: 'When Enable SSH tunnel' + hide: true + - field: sshTimeout + name: + zh-CN: SSH超时时间(ms) + en-US: SSH Timeout(ms) + ja-JP: SSHタイムアウト(ms) + type: number + required: false + range: '[400,200000]' + defaultValue: 6000 + hide: true + - field: sshUsername + name: + zh-CN: SSH用户名 + en-US: SSH Username + ja-JP: SSHユーザー名 + type: text + required: false + placeholder: 'When Enable SSH tunnel' + hide: true + - field: sshPassword + name: + zh-CN: SSH密码 + en-US: SSH Password + ja-JP: SSHパスワード + type: password + required: false + hide: true + - field: sshShareConnection + name: + zh-CN: 是否共享SSH连接 + en-US: Share SSH Connection + ja-JP: SSH接続共有 + type: boolean + required: true + defaultValue: true + hide: true + - field: sshPrivateKey + name: + zh-CN: SSH私钥 + en-US: SSH PrivateKey + ja-JP: SSH秘密鍵 + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + required: false + hide: true + - field: sshPrivateKeyPassphrase + name: + zh-CN: SSH密钥短语 + en-US: SSH PrivateKey PassPhrase + ja-JP: SSH秘密鍵フレーズ + type: password + required: false + hide: true + +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 基本信息 + en-US: Basic Info + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: server_version + type: 1 + label: true + i18n: + zh-CN: 服务器版本 + en-US: Server Version + ja-JP: バージョン + - field: port + type: 1 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: server_encoding + type: 1 + i18n: + zh-CN: 服务器编码 + en-US: Server Encoding + ja-JP: サーバーのエンコード + - field: data_directory + type: 1 + i18n: + zh-CN: 数据目录 + en-US: Data Directory + ja-JP: データディレクトリ + - field: max_connections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # host: ipv4 ipv6 host + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + # database platform name + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + # sql + sql: select name, setting as value from pg_settings where name = 'max_connections' or name = 'server_version' or name = 'server_encoding' or name = 'port' or name = 'data_directory'; + # JDBC url + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: state + i18n: + zh-CN: 状态信息 + en-US: State Info + ja-JP: 状態情報 + priority: 1 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: conflicts + type: 0 + unit: times + i18n: + zh-CN: 冲突次数 + en-US: Conflicts + ja-JP: コンフリクト回数 + - field: deadlocks + type: 0 + unit: times + i18n: + zh-CN: 死锁次数 + en-US: Deadlocks + ja-JP: デッドロック回数 + - field: blks_read + type: 0 + unit: blocks per second + i18n: + zh-CN: 读取块 + en-US: Blocks Read + ja-JP: 読み取られたブロック + - field: blks_hit + type: 0 + unit: blocks per second + i18n: + zh-CN: 命中块 + en-US: Blocks Hit + ja-JP: ヒットブロック + - field: blk_read_time + type: 0 + unit: ms + i18n: + zh-CN: 读取时间 + en-US: Read Time + ja-JP: 読み取られタイム + - field: blk_write_time + type: 0 + unit: ms + i18n: + zh-CN: 写入时间 + en-US: Write Time + ja-JP: 書き込まれ時間 + - field: stats_reset + type: 1 + i18n: + zh-CN: 统计重置 + en-US: Stats Reset + ja-JP: 統計リセット + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: SELECT COALESCE(datname,'shared-object') as db_name, conflicts, deadlocks, blks_read, blks_hit, blk_read_time, blk_write_time, stats_reset from pg_stat_database where (datname != 'template1' and datname != 'template0') or datname is null; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: activity + i18n: + zh-CN: 活动信息 + en-US: Activity Info + ja-JP: 活動情報 + priority: 2 + fields: + - field: running + type: 0 + unit: sbc + i18n: + zh-CN: 运行中 + en-US: Running + ja-JP: 実行中 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: SELECT count(*) as running FROM pg_stat_activity WHERE NOT pid=pg_backend_pid(); + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: resource_config + i18n: + zh-CN: 资源配置 + en-US: Resource Config + ja-JP: リソース設定 + priority: 3 + fields: + - field: work_mem + type: 0 + unit: MB + i18n: + zh-CN: 工作内存 + en-US: Work Memory + ja-JP: ワークメモリ + - field: shared_buffers + type: 0 + unit: MB + i18n: + zh-CN: 共享缓冲区 + en-US: Shared Buffers + ja-JP: 共有バッファ + - field: autovacuum + type: 1 + i18n: + zh-CN: 自动清理 + en-US: Auto Vacuum + ja-JP: オートバキューム + - field: max_connections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + - field: effective_cache_size + type: 0 + unit: MB + i18n: + zh-CN: 有效缓存大小 + en-US: Effective Cache Size + ja-JP: キャッシュサイズ + - field: wal_buffers + type: 0 + unit: MB + i18n: + zh-CN: WAL缓冲区 + en-US: WAL Buffers + ja-JP: WALバッファ + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: columns + sql: show all; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: connection + i18n: + zh-CN: 连接信息 + en-US: Connection Info + ja-JP: 接続情報 + priority: 4 + fields: + - field: active + type: 0 + i18n: + zh-CN: 活动连接 + en-US: Active Connection + ja-JP: 活躍的な接続 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: select count(1) as active from pg_stat_activity; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: connection_state + i18n: + zh-CN: 连接状态 + en-US: Connection State + ja-JP: 接続状態 + priority: 5 + fields: + - field: state + type: 1 + label: true + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + - field: num + type: 0 + i18n: + zh-CN: 数量 + en-US: Num + ja-JP: 数量 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select COALESCE(state, 'other') as state, count(*) as num from pg_stat_activity group by state; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: connection_db + i18n: + zh-CN: 连接数据库 + en-US: Connection Db + ja-JP: 接続データベース + priority: 6 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: active + type: 0 + i18n: + zh-CN: 活动连接 + en-US: Active Connection + ja-JP: 活躍的な接続 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select count(*) as active, COALESCE(datname, 'other') as db_name from pg_stat_activity group by datname; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: tuple + i18n: + zh-CN: 元组信息 + en-US: Tuple Info + ja-JP: 組情報 + priority: 7 + fields: + - field: fetched + type: 0 + i18n: + zh-CN: 获取次数 + en-US: Fetched + ja-JP: フェッチ回数 + - field: returned + type: 0 + i18n: + zh-CN: 返回次数 + en-US: Returned + ja-JP: 戻る回数 + - field: inserted + type: 0 + i18n: + zh-CN: 插入次数 + en-US: Inserted + ja-JP: インサート回数 + - field: updated + type: 0 + i18n: + zh-CN: 更新次数 + en-US: Updated + ja-JP: 更新回数 + - field: deleted + type: 0 + i18n: + zh-CN: 删除次数 + en-US: Deleted + ja-JP: 削除回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select sum(tup_fetched) as fetched, sum(tup_updated) as updated, sum(tup_deleted) as deleted, sum(tup_inserted) as inserted, sum(tup_returned) as returned from pg_stat_database; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: temp_file + i18n: + zh-CN: 临时文件 + en-US: Temp File + ja-JP: 一時ファイル + priority: 8 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: num + type: 0 + i18n: + zh-CN: 次数 + en-US: Num + ja-JP: 数量 + - field: Size + type: 0 + unit: B + i18n: + zh-CN: 大小 + en-US: Size + ja-JP: サイズ + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select COALESCE(datname, 'other') as db_name, sum(temp_files) as num, sum(temp_bytes) as size from pg_stat_database group by datname; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: lock + i18n: + zh-CN: 锁信息 + en-US: Lock Info + ja-JP: ロック情報 + priority: 9 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: conflicts + type: 0 + unit: times + i18n: + zh-CN: 冲突次数 + en-US: Conflicts + ja-JP: コンフリクト回数 + - field: deadlocks + type: 0 + unit: times + i18n: + zh-CN: 死锁次数 + en-US: Deadlocks + ja-JP: デッドロック回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: SELECT COALESCE(datname,'shared-object') as db_name, conflicts, deadlocks from pg_stat_database where (datname != 'template1' and datname != 'template0') or datname is null; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: slow_sql + i18n: + zh-CN: 慢查询 + en-US: Slow Sql + ja-JP: スローSQL + priority: 10 + fields: + - field: sql_text + type: 1 + label: true + i18n: + zh-CN: SQL语句 + en-US: SQL Text + ja-JP: SQL文のテキスト + - field: calls + type: 0 + i18n: + zh-CN: 调用次数 + en-US: Calls + ja-JP: コール回数 + - field: rows + type: 0 + i18n: + zh-CN: 行数 + en-US: Rows + ja-JP: 行 + - field: avg_time + type: 0 + unit: ms + i18n: + zh-CN: 平均时间 + en-US: Avg Time + ja-JP: 平均時間 + - field: total_time + type: 0 + unit: ms + i18n: + zh-CN: 总时间 + en-US: Total Time + ja-JP: 合計時間 + aliasFields: + - query + - calls + - rows + - total_exec_time + - mean_exec_time + calculates: + - sql_text=query + - avg_time=mean_exec_time + - total_time=total_exec_time + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select * from pg_stat_statements; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: transaction + i18n: + zh-CN: 事务信息 + en-US: Transaction Info + ja-JP: トランザクション情報 + priority: 11 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: commits + type: 0 + unit: times + i18n: + zh-CN: 提交次数 + en-US: Commits + ja-JP: コミット回数 + - field: rollbacks + type: 0 + unit: times + i18n: + zh-CN: 回滚次数 + en-US: Rollbacks + ja-JP: ロールバック回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select COALESCE(datname, 'other') as db_name, sum(xact_commit) as commits, sum(xact_rollback) as rollbacks from pg_stat_database group by datname; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: conflicts + i18n: + zh-CN: 冲突信息 + en-US: Conflicts Info + ja-JP: コンフリクト情報 + priority: 12 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: tablespace + type: 0 + i18n: + zh-CN: 表空间 + en-US: Tablespace + ja-JP: 表領域 + - field: lock + type: 0 + i18n: + zh-CN: 锁 + en-US: Lock + ja-JP: ロック + - field: snapshot + type: 0 + i18n: + zh-CN: 快照 + en-US: Snapshot + ja-JP: スナップショット + - field: bufferpin + type: 0 + i18n: + zh-CN: 缓冲区 + en-US: Bufferpin + ja-JP: バッファ + - field: deadlock + type: 0 + i18n: + zh-CN: 死锁 + en-US: Deadlock + ja-JP: デッドロック + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select datname as db_name, confl_tablespace as tablespace, confl_lock as lock, confl_snapshot as snapshot, confl_bufferpin as bufferpin, confl_deadlock as deadlock from pg_stat_database_conflicts; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: cache_hit_ratio + i18n: + zh-CN: 缓存命中率 + en-US: Cache Hit Ratio + ja-JP: キャッシュ命中率 + priority: 13 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: ratio + type: 0 + unit: '%' + i18n: + zh-CN: 命中率 + en-US: Hit Ratio + ja-JP: 命中率 + aliasFields: + - blks_hit + - blks_read + - db_name + calculates: + - ratio=(blks_hit + 1) / (blks_read + blks_hit + 1) * 100 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select datname as db_name, blks_hit, blks_read from pg_stat_database; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: checkpoint + i18n: + zh-CN: Checkpoint信息 + en-US: Checkpoint Info + ja-JP: チェックポイント情報 + priority: 14 + fields: + - field: checkpoint_sync_time + type: 0 + unit: ms + i18n: + zh-CN: Checkpoint同步时间 + en-US: Checkpoint Sync Time + ja-JP: チェックポイント同期時間 + - field: checkpoint_write_time + type: 0 + unit: ms + i18n: + zh-CN: Checkpoint写入时间 + en-US: Checkpoint Write Time + ja-JP: Checkpoint書き込まれた時間 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: select checkpoint_sync_time, checkpoint_write_time from pg_stat_bgwriter; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: buffer + i18n: + zh-CN: Buffer信息 + en-US: Buffer Info + ja-JP: バッファ情報 + priority: 15 + fields: + - field: allocated + type: 0 + i18n: + zh-CN: 已分配 + en-US: Allocated + ja-JP: 割り当てバッファ + - field: fsync_calls_by_backend + type: 0 + i18n: + zh-CN: 后端进程直接执行的文件同步调用次数 + en-US: Fsync Calls By Backend + ja-JP: バックエンド同期コール回数 + - field: written_directly_by_backend + type: 0 + i18n: + zh-CN: 后台写入到数据文件 + en-US: Written Directly By Backend + ja-JP: バックエンドによる直接書き込まれたファイル + - field: written_by_background_writer + type: 0 + i18n: + zh-CN: 后台写入 + en-US: Written By Background Writer + ja-JP: バックグラウンドライターに書き込まれた + - field: written_during_checkpoints + type: 0 + i18n: + zh-CN: 检查点期间写入 + en-US: Written During Checkpoints + ja-JP: チェックポイント中の書き込み + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: select buffers_alloc as allocated, buffers_backend_fsync as fsync_calls_by_backend, buffers_backend as written_directly_by_backend, buffers_clean as written_by_background_writer, buffers_checkpoint as written_during_checkpoints from pg_stat_bgwriter; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: users + i18n: + zh-CN: 用户信息 + en-US: Users + ja-JP: ユーザー情報 + priority: 16 + fields: + - field: usename + type: 1 + label: true + i18n: + zh-CN: 用户名 + en-US: User Name + ja-JP: ユーザー名 + - field: usesysid + type: 0 + i18n: + zh-CN: 用户ID + en-US: User ID + ja-JP: ユーザーID + - field: usecreatedb + type: 1 + i18n: + zh-CN: 创建数据库权限 + en-US: Create DB Privilege + ja-JP: データベース作成権限 + - field: usesuper + type: 1 + i18n: + zh-CN: 超级用户权限 + en-US: Superuser Privilege + ja-JP: スーパーユーザー権限 + - field: valuntil + type: 1 + i18n: + zh-CN: 密码有效期 + en-US: Password Valid Until + ja-JP: パスワード有効期限 + - field: days_left + type: 0 + i18n: + zh-CN: 剩余天数 + en-US: Days Left + ja-JP: 残り日数 + - field: account_status + type: 1 + i18n: + zh-CN: 账号状态 + en-US: Account Status + ja-JP: アカウントのステータス + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: | + SELECT + rolname AS usename, + oid AS usesysid, + CASE WHEN rolcreatedb THEN 'Y' ELSE 'N' END as usecreatedb, + CASE WHEN rolsuper THEN 'Y' ELSE 'N' END as usesuper, + rolvaliduntil AS valuntil, + CASE + WHEN rolvaliduntil IS NULL THEN NULL + WHEN rolvaliduntil = 'infinity'::timestamptz THEN NULL + WHEN rolvaliduntil = '-infinity'::timestamptz THEN NULL + ELSE EXTRACT(DAY FROM (rolvaliduntil - NOW()))::INTEGER + END as days_left, + CASE + WHEN rolvaliduntil IS NULL THEN 'OPEN' + WHEN rolvaliduntil = 'infinity'::timestamptz THEN 'OPEN' + WHEN rolvaliduntil = '-infinity'::timestamptz THEN 'EXPIRED' + WHEN rolvaliduntil < NOW() THEN 'EXPIRED' + ELSE 'OPEN' + END as account_status + FROM pg_roles + WHERE rolcanlogin = true + ORDER BY days_left ASC NULLS LAST; + url: ^_^url^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + diff --git a/hertzbeat-manager/src/main/resources/define/app-prestodb.yml b/hertzbeat-manager/src/main/resources/define/app-prestodb.yml new file mode 100644 index 0000000..120e92a --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-prestodb.yml @@ -0,0 +1,377 @@ +# 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. + +# The monitoring type category: service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: prestodb +# The app api i18n name +name: + zh-CN: PrestoDB + en-US: PrestoDB + ja-JP: PrestoDB +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 PrestoDB 数据库平台的性能指标进行监测。您可以点击“新增 PrestoDB”按钮并进行配置,或在“更多操作”中导入已有配置。 + en-US: Hertzbeat monitors performance metrics of the PrestoDB database platform. You can click the "Add PrestoDB" button and proceed with the configuration or import an existing setup through the "More Actions" menu. + ja-JP: Hertzbeat は PrestoDBの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 PrestoDB</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/prestodb + en-US: https://hertzbeat.apache.org/docs/help/prestodb +params: + - field: host + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + required: true + - field: ssl + name: + zh-CN: 启用HTTPS + en-US: HTTPS + ja-JP: HTTPS + type: boolean + required: true + - field: timeout + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + type: number + required: false + hide: true + defaultValue: 6000 + +metrics: + - name: cluster + i18n: + zh-CN: 集群状态 + en-US: Cluster Status + ja-JP: クラスターステータス情報 + priority: 0 + fields: + - field: activeWorkers + type: 0 + i18n: + zh-CN: 活跃节点数 + en-US: Active Workers + ja-JP: アクティブなワーカー数 + - field: runningQueries + type: 0 + i18n: + zh-CN: 运行中的查询数 + en-US: Running Queries + ja-JP: 実行中のクエリ数 + - field: queuedQueries + type: 0 + i18n: + zh-CN: 队列中的查询数 + en-US: Queued Queries + ja-JP: キュー内のクエリ数 + - field: blockedQueries + type: 0 + i18n: + zh-CN: 阻塞的查询数 + en-US: Blocked Queries + ja-JP: ブロックされたクエリ数 + - field: runningDrivers + type: 0 + i18n: + zh-CN: 运行中的驱动数 + en-US: Running Drivers + ja-JP: 実行中のドライバー数 + - field: runningTasks + type: 0 + i18n: + zh-CN: 运行中的任务数 + en-US: Running Tasks + ja-JP: 実行中のタスク数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /v1/cluster + timeout: ^_^timeout^_^ + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$' + + - name: node + i18n: + zh-CN: 节点信息 + en-US: Node Info + ja-JP: ノード情報 + priority: 1 + fields: + - field: uri + type: 1 + i18n: + zh-CN: 节点链接 + en-US: Node Uri + ja-JP: ノードURI + - field: recentRequests + type: 0 + i18n: + zh-CN: 最近一段时间内的请求数量 + en-US: Recent Requests + ja-JP: 最近のリクエスト数 + - field: recentFailures + type: 0 + i18n: + zh-CN: 最近一段时间内的失败请求数量 + en-US: Recent Failures + ja-JP: 最近のリクエスト失敗数 + - field: recentSuccesses + type: 0 + i18n: + zh-CN: 最近一段时间内的成功请求数量 + en-US: Recent Successes + ja-JP: 最近のリクエスト成功数 + - field: lastRequestTime + type: 1 + i18n: + zh-CN: 最近一次请求的时间 + en-US: Last Request Time + ja-JP: 最後のリクエスト時刻 + - field: lastResponseTime + type: 1 + i18n: + zh-CN: 最近一次响应的时间 + en-US: Last Response Time + ja-JP: 最後の応答時刻 + - field: age + type: 0 + i18n: + zh-CN: 持续时间 + en-US: Age + ja-JP: スパン + - field: recentFailureRatio + type: 0 + i18n: + zh-CN: 最近一段时间内的失败 + en-US: Recent Failure Ratio + ja-JP: 最近の失敗比率 + + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /v1/node + timeout: ^_^timeout^_^ + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$[*]' + + - name: status + i18n: + zh-CN: 节点状态 + en-US: Node Status + ja-JP: ノードステータス情報 + priority: 2 + fields: + - field: nodeId + type: 1 + i18n: + zh-CN: 节点ID + en-US: Node ID + ja-JP: ノードID + - field: nodeVersion + type: 1 + i18n: + zh-CN: 节点版本 + en-US: Node Version + ja-JP: Nodeバージョン + - field: environment + type: 1 + i18n: + zh-CN: 环境 + en-US: Environment + ja-JP: 環境 + - field: coordinator + type: 1 + i18n: + zh-CN: 是否为协调节点 + en-US: Is Coordinator + ja-JP: コーディネーターノード + - field: uptime + type: 1 + i18n: + zh-CN: 正常运行时间 + en-US: Uptime + ja-JP: アップタイム + - field: externalAddress + type: 1 + i18n: + zh-CN: 外部地址 + en-US: External Address + ja-JP: 外部アドレス + - field: internalAddress + type: 1 + i18n: + zh-CN: 内部地址 + en-US: Internal Address + ja-JP: 内部アドレス + - field: processors + type: 0 + i18n: + zh-CN: 处理器数量 + en-US: Processors + ja-JP: プロセッサ数 + - field: processCpuLoad + type: 0 + i18n: + zh-CN: 进程CPU负载 + en-US: Process CPU Load + ja-JP: プロセスのCPUロード + - field: systemCpuLoad + type: 0 + i18n: + zh-CN: 系统CPU负载 + en-US: System CPU Load + ja-JP: システムのCPUロード + - field: heapUsed + type: 0 + i18n: + zh-CN: 已使用堆内存 + en-US: Heap Memory Used + ja-JP: 使用したヒープメモリ + unit: MB + - field: heapAvailable + type: 0 + i18n: + zh-CN: 可用堆内存 + en-US: Heap Memory Available + ja-JP: 使用可能なヒープメモリ + unit: MB + - field: nonHeapUsed + type: 0 + i18n: + zh-CN: 已使用非堆内存 + en-US: Non-Heap Memory Used + ja-JP: 使用したノンヒープメモリ + unit: MB + units: + - heapUsed=B->MB + - heapAvailable=B->MB + - nonHeapUsed=B->MB + aliasFields: + - $.nodeId + - $.nodeVersion.version + - $.environment + - $.coordinator + - $.uptime + - $.externalAddress + - $.internalAddress + - $.processors + - $.processCpuLoad + - $.systemCpuLoad + - $.heapUsed + - $.heapAvailable + - $.nonHeapUsed + calculates: + - nodeId = $.nodeId + - nodeVersion = $.nodeVersion.version + - environment = $.environment + - coordinator = $.coordinator + - uptime = $.uptime + - externalAddress = $.externalAddress + - internalAddress = $.internalAddress + - processors = $.processors + - processCpuLoad = $.processCpuLoad + - systemCpuLoad = $.systemCpuLoad + - heapUsed = $.heapUsed + - heapAvailable = $.heapAvailable + - nonHeapUsed = $.nonHeapUsed + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /v1/status + timeout: ^_^timeout^_^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$' + + - name: task + i18n: + zh-CN: 任务查询 + en-US: Task Query + ja-JP: タスククエリ + priority: 3 + fields: + - field: taskId + type: 1 + i18n: + zh-CN: 任务ID + en-US: Task ID + ja-JP: タスクID + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + - field: state + type: 1 + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + - field: self + type: 1 + i18n: + zh-CN: 自身 + en-US: Self + ja-JP: 自分 + - field: lastHeartbeat + type: 1 + i18n: + zh-CN: 最后心跳时间 + en-US: Last Heartbeat + ja-JP: 最後のハートビート + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /v1/task + timeout: ^_^timeout^_^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$[*]' diff --git a/hertzbeat-manager/src/main/resources/define/app-process.yml b/hertzbeat-manager/src/main/resources/define/app-process.yml new file mode 100644 index 0000000..aa22b58 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-process.yml @@ -0,0 +1,623 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: program +# The monitoring type eg: linux windows tomcat mysql aws... +app: process +# The monitoring i18n name +name: + zh-CN: Linux进程 + en-US: Linux Process + ja-JP: Linuxプロセス +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 Linux系统进程进行监控,支持根据进程名称(或部分名称)匹配进行监控,支持进程的 CPU使用率、内存使用率、物理内存、IO 等监控。<br>您可以点击“<i>新建 Linux进程</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'>SSH protocol</a> to monitor Linux system processes. It supports monitoring based on process names (or partial names), and provides monitoring for CPU usage, memory usage, physical memory, IO, and more. <br> You can click on "<i>New Linux Process</i>" and configure HOST port, account, and other related parameters for addition. It supports SSH account password or key authentication. Alternatively, you can choose "<i>More Actions</i>" to import existing configurations. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'>SSH 協議</a> 對 Linux 系統進程進行監控,支持根據進程名稱(或部分名稱)匹配進行監控,支持進程的 CPU 使用率、內存使用率、物理內存、IO 等監控。<br>您可以點擊“新建 Linux 进程”,並配置 HOST 端口、帳戶等相關參數進行添加,支持 SSH 帳戶密碼或金鑰認證。或者選擇“更多操作”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> Linux システムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Linuxプロセス</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/process/ + en-US: https://hertzbeat.apache.org/docs/help/process/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + - field: process_name + # name-param field display i18n name + name: + zh-CN: 进程名称 + en-US: process_name + ja-JP: プロセス名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 进程基本信息 + en-US: Basic Info + ja-JP: プロセス情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: user + type: 1 + label: true + i18n: + zh-CN: 用户 + en-US: User + ja-JP: ユーザー + - field: cpu + type: 0 + i18n: + zh-CN: CPU使用率 + en-US: CPU + ja-JP: CPU使用率 + - field: mem + type: 0 + i18n: + zh-CN: 内存使用率 + en-US: MEM + ja-JP: メモリ使用率 + - field: rss + type: 0 + unit: MB + i18n: + zh-CN: 物理内存 + en-US: rss + ja-JP: 物理メモリ + - field: cmd + type: 1 + i18n: + zh-CN: 运行命令 + en-US: cmd + ja-JP: コマンド + units: + - rss=KB->MB + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + # ssh response data parse type: oneRow, multiRow + script: output=$(ps -ef|grep '^_^process_name^_^'|grep -v grep); [ -n "$output" ] && ps -eo pid,user,%cpu,%mem,rss,cmd | grep -v grep | grep '^_^process_name^_^' | awk 'BEGIN {print "pid user cpu mem rss cmd"} {cmd=substr($0, index($0, $6)); gsub(/ /, " ", cmd); print $1, $2, $3, $4, $5, cmd}' + parseType: multiRow + - name: mem + i18n: + zh-CN: 内存使用信息 + en-US: MEM + ja-JP: メモリ情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + + priority: 1 + # collect metrics content + + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: VmPeak + type: 1 + label: true + i18n: + zh-CN: 虚拟内存峰值 + en-US: VmPeak + ja-JP: 仮想メモリの最大値 + unit: kB + - field: VmSize + type: 1 + label: true + i18n: + zh-CN: 虚拟内存 + en-US: VmSize + ja-JP: 仮想メモリ + unit: kB + - field: VmLck + type: 1 + label: true + i18n: + zh-CN: 锁定内存 + en-US: VmLck + ja-JP: 物理メモリのロック + unit: kB + - field: VmPin + type: 1 + label: true + i18n: + zh-CN: 固定内存 + en-US: VmPin + ja-JP: 物理メモリの固定値 + unit: kB + - field: VmHWM + type: 1 + label: true + i18n: + zh-CN: 物理内存峰值 + en-US: VmHWM + ja-JP: 物理メモリの最大値 + unit: kB + - field: VmRSS + type: 1 + label: true + i18n: + zh-CN: 物理内存使用 + en-US: VmRSS + ja-JP: 使用した物理メモリ + unit: kB + - field: VmData + type: 1 + label: true + i18n: + zh-CN: 数据段大小 + en-US: VmData + ja-JP: 仮想メモリのページ + unit: kB + - field: VmStk + type: 1 + label: true + i18n: + zh-CN: 堆栈大小 + en-US: VmStk + ja-JP: スタック領域のサイズ + unit: kB + - field: VmExe + type: 1 + label: true + i18n: + zh-CN: 代码大小 + en-US: VmExe + ja-JP: 実行可能な仮想メモリ + unit: kB + - field: VmLib + type: 1 + label: true + i18n: + zh-CN: 共享库大小 + en-US: VmLib + ja-JP: ライブラリのサイズ + unit: kB + - field: VmPTE + type: 1 + label: true + i18n: + zh-CN: 页表项大小 + en-US: VmPTE + ja-JP: ページテーブルエントリのサイズ + unit: kB + - field: VmSwap + type: 1 + label: true + i18n: + zh-CN: 交换空间 + en-US: VmSwap + ja-JP: スワップ領域 + unit: kB + + + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + # ssh response data parse type: oneRow, multiRow + script: echo -e "pid VmPeak VmSize VmLck VmPin VmHWM VmRSS VmData VmStk VmExe VmLib VmPTE VmSwap";ps -eo pid,cmd | grep -v grep | grep '^_^process_name^_^' | awk '{ cmd=substr($0, index($0, $3)); sub(/ /, " ", cmd); print $1, cmd }' | while read pid _; do echo -n "$pid ";cat /proc/$pid/status | grep Vm | awk -F ':\t' '{gsub(/^ +| +$/, "", $2); print $2}' | paste -d' ' -s - | sed 's/[[:space:]]*[kKMmGg]*B//g';done + parseType: multiRow + + - name: other + i18n: + zh-CN: 其他监控信息 + en-US: Other + ja-JP: その他の情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: path + type: 1 + i18n: + zh-CN: 执行路径 + en-US: path + ja-JP: 実行パス + - field: date + type: 1 + i18n: + zh-CN: 启动时间 + en-US: date + ja-JP: 起動時間 + - field: fd_count + type: 1 + i18n: + zh-CN: 打开文件描述符数量 + en-US: fd_count + ja-JP: ファイル記述子数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + # ssh response data parse type: oneRow, multiRow + script: echo "pid path date fd_count" ; ps -eo pid,cmd | grep -v grep | grep '^_^process_name^_^' | awk '{cmd=substr($0, index($0, $3)); gsub(/ /, " ", cmd); print $1, cmd}' | while read pid _; do cwd=$(readlink -f "/proc/$pid/cwd"); start_time=$(ps -p $pid -o lstart | tail -n 1 | sed 's/ / /g'); fd_count=$(ls -l /proc/$pid/fd/ 2>/dev/null | wc -l); echo "$pid $cwd $start_time $fd_count"; done + parseType: multiRow + + - name: io + i18n: + zh-CN: IO + en-US: IO + ja-JP: IO + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: rchar + type: 1 + i18n: + zh-CN: 字符读取数 + en-US: rchar + ja-JP: チャーの読み取り数 + - field: wchar + type: 1 + i18n: + zh-CN: 字符写入数 + en-US: wchar + ja-JP: チャーの書き込み数 + - field: syscr + type: 1 + i18n: + zh-CN: 系统读取次数 + en-US: syscr + ja-JP: システムの読み取り回数 + - field: syscw + type: 1 + i18n: + zh-CN: 系统写入次数 + en-US: syscw + ja-JP: システムの書き込み回数 + - field: read_bytes + type: 1 + i18n: + zh-CN: 读取字节数 + en-US: read_bytes + ja-JP: 読み取りバイト数 + - field: write_bytes + type: 1 + i18n: + zh-CN: 写入字节数 + en-US: write_bytes + ja-JP: 書き込みバイト数 + - field: cancelled_write_bytes + type: 1 + i18n: + zh-CN: 取消写入字节数 + en-US: cancelled_write_bytes + ja-JP: 書き込みをキャンセルするバイト数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + # ssh response data parse type: oneRow, multiRow + script: echo -e "pid rchar wchar syscr syscw read_bytes write_bytes cancelled_write_bytes";ps -eo pid,cmd | grep -v grep | grep '^_^process_name^_^' | awk '{ cmd=substr($0, index($0, $3)); gsub(/ /, " ", cmd); print $1, cmd }' | while read pid _; do echo -n "$pid ";cat /proc/$pid/io | awk -F ':' '{gsub(/^ +| +$/, "", $2); print $2}' | paste -d' ' -s -;done + parseType: multiRow + diff --git a/hertzbeat-manager/src/main/resources/define/app-prometheus.yml b/hertzbeat-manager/src/main/resources/define/app-prometheus.yml new file mode 100644 index 0000000..92b2b06 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-prometheus.yml @@ -0,0 +1,224 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: auto +# The monitoring type eg: linux windows tomcat mysql aws... +app: prometheus +# The monitoring i18n name +name: + zh-CN: Prometheus任务 + en-US: Prometheus Task + ja-JP: Prometheusタスク +# The description and help of this monitoring type +help: + zh-CN: Prometheus 任务管理,您可以通过此监控支持 Prometheus 协议的应用类型数据,像 Prometheus 监控它们一样方便,并且是可视化页面的操作。<br><span class='help_module_span'>⚠️注意:监控对端需要提供 prometheus metrics 协议的数据接口。</span> + en-US: Prometheus Task Management, you can use this to monitor application that supports the Prometheus protocol, as convenient as Prometheus monitoring them, and it is a visual page operation. <br><span class='help_module_span'> The monitoring peer needs to provide the data interface of the prometheus metrics protocol. </span> + zh-TW: Prometheus 任務管理,您可以透過此監控支援 Prometheus 協定的應用程式類型數據,像 Prometheus 監控它們一樣方便,並且是視覺化頁面的操作。 <br><span class='help_module_span'>⚠️注意:監控對端需要提供 prometheus metrics 協定的資料介面。 </span> + ja-JP: プロメテウス・タスク管理では、プロメテウス・プロトコルをサポートするアプリケーション・タイプのデータを、プロメテウスが監視するのと同じように簡単に監視することができ、ビジュアルなページでそれを行うことができます。<br><span class='help_module_span'>⚠️注意:prometheus metrics プロトコルへのデータ・インターフェースを提供する必要があります。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/prometheus/ + en-US: https://hertzbeat.apache.org/docs/help/prometheus/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # default value + defaultValue: 9091 + required: true + # field-param field key + - field: path + # name-param field display i18n name + name: + zh-CN: 端点路径 + en-US: Endpoint Path + ja-JP: エンドポイントパス + # type-param field type(most mapping the html input type) + type: text + # param field input placeholder + placeholder: 'Exporter Url Endpoint Path' + defaultValue: "/metrics" + # required-true or false + required: true + - field: timeout + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + type: number + required: false + # hide param-true or false + hide: true + defaultValue: 6000 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启用HTTPS + en-US: HTTPS + ja-JP: HTTPS + # type-param field type(most mapping the html input type) + type: boolean + hide: true + # field-param field key + - field: headers + # name-param field display i18n name + name: + zh-CN: 请求Headers + en-US: Headers + ja-JP: ヘッダ + # type-param field type(most mapping the html input type) + type: key-value + # required-true or false + required: false + hide: true + # when type is key-value, use keyAlias to config key alias name + keyAlias: Header Name + # when type is key-value, use valueAlias to config value alias name + valueAlias: Header Value + # field-param field key + - field: params + # name-param field display i18n name + name: + zh-CN: 查询Params + en-US: Params + ja-JP: パラメータ + # type-param field type(most mapping the html input type) + type: key-value + # required-true or false + required: false + hide: true + # when type is key-value, use keyAlias to config key alias name + keyAlias: Param Key + # when type is key-value, use valueAlias to config value alias name + valueAlias: Param Value + # field-param field key + - field: contentType + # name-param field display i18n name + name: + zh-CN: Content-Type + en-US: Content-Type + ja-JP: コンテンツタイプ + # type-param field type(most mapping the html input type) + type: text + # param field input placeholder + placeholder: 'Request Body Type' + # required-true or false + required: false + # hide param-true or false + hide: true + - field: authType + # name-param field display i18n name + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + # type-param field type(most mapping the html input type) + type: radio + # required-true or false + required: false + # hide param-true or false + hide: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - cluster_node_status + - name: auto + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: prometheus + # the config content when protocol is http + prometheus: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http path + path: ^_^path^_^ + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + # http request payload + payload: ^_^payload^_^ + # http request header content + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + # http request params + params: + ^_^params^_^: ^_^params^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-pulsar.yml b/hertzbeat-manager/src/main/resources/define/app-pulsar.yml new file mode 100644 index 0000000..133df9b --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-pulsar.yml @@ -0,0 +1,411 @@ +# 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. + +# The monitoring type category: service-application service monitoring, db-database monitoring, custom-custom monitoring. +category: mid +# The monitoring application type name (consistent with the file name) +app: pulsar +name: + zh-CN: Pulsar + en-US: Pulsar + ja-JP: Pulsar +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 从 Pulsar WebService接口查询到 Pulsar 的通用指标数据来进行监控。<br>您可以点击 “<i>新建 Pulsar</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat queries general metric data from Pulsar through the Pulsar WebService interface for monitoring. You can click "<i>Create Pulsar</i>" to configure a new monitoring task, or select "<i>More Actions</i>" to import existing configurations. + zh-TW: Hertzbeat 從 Pulsar WebService接口查詢到 Pulsar 的通用指標數據來進行監控。<br>您可以點擊 “<i>新建 Pulsar</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeatは、Pulsar WebServiceを呼び出してPulsarの一般的なメトリクスを監視します。<br>「<i>新規 Pulsar</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/pulsar + en-US: https://hertzbeat.apache.org/docs/help/pulsar +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # default value + defaultValue: 8030 + required: true + - field: timeout + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + type: number + required: false + # hide param-true or false + hide: true + defaultValue: 6000 +metrics: + - name: pulsar_version_info + i18n: + zh-CN: 版本信息 + en-US: Version Info + ja-JP: バージョン情報 + priority: 0 + fields: + - field: version + i18n: + zh-CN: 版本信息 + en-US: Version Info + ja-JP: バージョン情報 + type: 1 + label: false + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: process_start_time_seconds + i18n: + zh-CN: 进程启动时间 + en-US: Process Start Time + ja-JP: プロセスの起動時間 + priority: 1 + fields: + - field: value + i18n: + zh-CN: 进程启动时间 + en-US: Process Start Time + ja-JP: プロセスの起動時間 + type: 0 + label: false + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: process_open_fds + i18n: + zh-CN: 打开的文件描述符 + en-US: Open File Descriptors + ja-JP: 開かれたファイル記述子 + priority: 2 + fields: + - field: value + type: 0 + i18n: + zh-CN: 打开的文件描述符 + en-US: Open File Descriptors + ja-JP: 開かれたファイル記述子 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: process_max_fds + i18n: + zh-CN: 最大文件描述符 + en-US: Max File Descriptors + ja-JP: ファイル記述子の最大数 + priority: 3 + fields: + - field: value + type: 0 + i18n: + zh-CN: 最大文件描述符 + en-US: Max File Descriptors + ja-JP: ファイル記述子の最大数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: jvm_memory_pool_allocated_bytes_total + i18n: + zh-CN: JVM 内存池已分配字节 + en-US: JVM Memory Pool Allocated Bytes + ja-JP: Java仮想マシンのメモリプールが割り当てられたバイト数 + priority: 4 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + fields: + - field: pool + type: 1 + label: true + i18n: + zh-CN: 池 + en-US: Pool + ja-JP: プール + - field: value + type: 0 + i18n: + zh-CN: 值 + en-US: Value + ja-JP: 値 + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: jvm_memory_pool_bytes_used + i18n: + zh-CN: JVM 内存池已使用字节 + en-US: JVM Memory Pool Used Bytes + ja-JP: Java仮想マシンのメモリプールが使用したバイト数 + priority: 5 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + fields: + - field: pool + type: 1 + i18n: + zh-CN: 池 + en-US: Pool + ja-JP: プール + label: true + - field: value + type: 0 + i18n: + zh-CN: 值 + en-US: Value + ja-JP: 値 + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: jvm_memory_pool_bytes_committed + i18n: + zh-CN: JVM 内存池已提交字节 + en-US: JVM Memory Pool Committed Bytes + ja-JP: Java仮想マシンのメモリプールがコミットされたバイト数 + priority: 6 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + fields: + - field: pool + type: 1 + i18n: + zh-CN: 池 + en-US: Pool + ja-JP: プール + label: true + - field: value + type: 0 + i18n: + zh-CN: 值 + en-US: Value + ja-JP: 値 + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: jvm_memory_pool_bytes_max + i18n: + zh-CN: JVM 内存池最大字节 + en-US: JVM Memory Pool Max Bytes + ja-JP: Java仮想マシンのメモリプールの最大バイト数 + priority: 7 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + fields: + - field: pool + type: 1 + i18n: + zh-CN: 池 + en-US: Pool + ja-JP: プール + label: true + - field: value + type: 0 + i18n: + zh-CN: 值 + en-US: Value + ja-JP: 値 + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: pulsar_broker_publish_latency + i18n: + zh-CN: broker 发布延迟 + en-US: Broker Publish Latency + ja-JP: ブローカーの転送レイテンシー + priority: 8 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + fields: + - field: cluster + type: 1 + i18n: + zh-CN: 集群 + en-US: Cluster + ja-JP: クラスター + label: true + - field: quantile + type: 1 + i18n: + zh-CN: 分位数 + en-US: Quantile + ja-JP: 分位数 + label: true + - field: value + type: 0 + i18n: + zh-CN: 值 + en-US: Value + ja-JP: 値 + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + - name: pulsar_metadata_store_ops_latency_ms_bucket + i18n: + zh-CN: 元数据存储操作延迟 + en-US: Metadata Store Ops Latency + ja-JP: メタデータストレージする操作のレイテンシー + priority: 9 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + fields: + - field: cluster + type: 1 + i18n: + zh-CN: 集群 + en-US: Cluster + ja-JP: クラスター + label: true + - field: name + type: 1 + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + label: true + - field: type + type: 1 + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: タイプ + label: true + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + label: true + - field: le_label + type: 1 + - field: value + type: 0 + i18n: + zh-CN: 值 + en-US: value + ja-JP: 値 + aliasFields: + - cluster + - name + - type + - status + - le + - value + calculates: + - le_label=le + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus diff --git a/hertzbeat-manager/src/main/resources/define/app-push.yml b/hertzbeat-manager/src/main/resources/define/app-push.yml new file mode 100644 index 0000000..0ade060 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-push.yml @@ -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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: __system__ +# Monitoring application type name (consistent with file name) eg: linux windows tomcat mysql aws... +app: push +# The app api i18n name +name: + zh-CN: 推送方式监控 + en-US: Push Style Monitor + ja-JP: プッシュ方法のモニター +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 推送模块Host + en-US: Push Module Host + ja-JP: プッシュモジュールのホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + defaultValue: 127.0.0.1 + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 1157 + - field: fields + # name-param field display i18n name + name: + zh-CN: 监控数据字段 + en-US: Metrics fields + ja-JP: メトリクスフィールド + # type-param field type(most mapping the html input type) + type: metrics-field + # required-true or false + required: true + +# collect metrics config list +metrics: + # metrics - all + - name: metrics + i18n: + zh-CN: 指标 + en-US: Metrics + ja-JP: メトリクス + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: push + # the config content when protocol is http + push: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http uri + uri: /api/push diff --git a/hertzbeat-manager/src/main/resources/define/app-qq_mailbox.yml b/hertzbeat-manager/src/main/resources/define/app-qq_mailbox.yml new file mode 100644 index 0000000..e777c67 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-qq_mailbox.yml @@ -0,0 +1,446 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: service +# Monitoring application type name (consistent with file name) eg: linux windows tomcat mysql aws... +app: qq_mailbox +# The app api i18n name +name: + zh-CN: QQ 邮箱监控 + en-US: QQ Mailbox + ja-JP: QQ電子メール +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 IMAP 协议对 QQ 邮箱的详细信息进行采集监控。<br>您可以点击“<i>新建 QQ 邮箱</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses IMAP protocol to collect detailed metrics of QQ mailbox. <br>You can click "<i>New QQ Mailbox</i>" and configure it, or select "<i>More Operations</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 IMAP 協議對 QQ 郵箱的詳細信息進行採集監控。<br>您可以點擊“<i>新建 QQ 邮箱</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: HertzBeat は IMAP プロトコルを介してQQ電子メールの詳細情報を監視します。<br>「<i>新規 QQ電子メール</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/imap + en-US: https://hertzbeat.apache.org/docs/help/imap +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL + # When the type is boolean, the frontend will display a switch for it. + type: boolean + # required-true or false + required: false + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 + # field-param field key + - field: email + # name-param field display i18n name + name: + zh-CN: IMAP 邮箱地址 + en-US: IMAP Address + ja-JP: IMAPアドレス + # type-param field type(most mapping the html input type) The type "text" belongs to a text input field. + type: text + # required-true or false + required: true + # field-param field key + - field: authorize + # name-param field display i18n name + name: + zh-CN: 授权码 + en-US: Authorize Code + ja-JP: 認証コード + # type-param field type(most mapping the html input type) The type "text" belongs to a text input field. + type: text + # required-true or false + required: true + +# collect metrics config list +metrics: + # metrics - available + - name: available + i18n: + zh-CN: 可用性 + en-US: Available + ja-JP: 可用性 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: placeholder + + - name: inboxInfo + i18n: + zh-CN: 收件箱信息 + en-US: Inbox info + ja-JP: 受信箱情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: allMessageCount + type: 0 + i18n: + zh-CN: 邮件总数 + en-US: Total message count + ja-JP: メッセージ総数 + - field: recentMessageCount + type: 0 + i18n: + zh-CN: 最近收到邮件总数 + en-US: Recent message count + ja-JP: 最新メッセージ総数 + - field: unseenMessageCount + type: 0 + i18n: + zh-CN: 未读邮件总数 + en-US: Unseen message count + ja-JP: 未読メッセージ総数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - INBOXTotalMessageCount + - INBOXRecentMessageCount + - INBOXUnseenMessageCount + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + calculates: + - allMessageCount = INBOXTotalMessageCount + - recentMessageCount = INBOXRecentMessageCount + - unseenMessageCount = INBOXUnseenMessageCount + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: INBOX + + - name: sentInfo + i18n: + zh-CN: 已发送信息 + en-US: Sent info + ja-JP: 送信情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: allMessageCount + type: 0 + i18n: + zh-CN: 邮件总数 + en-US: Total message count + ja-JP: 送信したメッセージ総数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - Sent MessagesTotalMessageCount + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + calculates: + - allMessageCount = Sent MessagesTotalMessageCount + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: Sent Messages + + - name: draftsInfo + i18n: + zh-CN: 草稿箱信息 + en-US: Drafts info + ja-JP: ドラフトボックス情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: allMessageCount + type: 0 + i18n: + zh-CN: 邮件总数 + en-US: Total message count + ja-JP: ドラフトメッセージ総数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - DraftsTotalMessageCount + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + calculates: + - allMessageCount = DraftsTotalMessageCount + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: Drafts + + - name: deletedMessagesInfo + i18n: + zh-CN: 已删除信息 + en-US: Deleted Messages info + ja-JP: 削除済みメッセージ情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 4 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: allMessageCount + type: 0 + i18n: + zh-CN: 邮件总数 + en-US: Total message count + ja-JP: 削除済みメッセージ総数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - Deleted MessagesTotalMessageCount + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + calculates: + - allMessageCount = Deleted MessagesTotalMessageCount + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: Deleted Messages + + - name: JunkInfo + i18n: + zh-CN: 垃圾箱信息 + en-US: Junk info + ja-JP: 迷惑メールボックス情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 5 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: allMessageCount + type: 0 + i18n: + zh-CN: 邮件总数 + en-US: Total message count + ja-JP: 迷惑メッセージ総数 + - field: recentMessageCount + type: 0 + i18n: + zh-CN: 最近收到邮件总数 + en-US: Recent message count + ja-JP: 最新の迷惑メッセージ総数 + - field: unseenMessageCount + type: 0 + i18n: + zh-CN: 未读邮件总数 + en-US: Unseen message count + ja-JP: 未読の迷惑メッセージ総数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - JunkTotalMessageCount + - JunkRecentMessageCount + - JunkUnseenMessageCount + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + calculates: + - allMessageCount = JunkTotalMessageCount + - recentMessageCount = JunkRecentMessageCount + - unseenMessageCount = JunkUnseenMessageCount + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: Junk + + - name: QQMailInfo + i18n: + zh-CN: QQ邮件订阅信息 + en-US: QQ Mail Subscribe Info + ja-JP: QQ電子メールサブスクリプション情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 6 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: allMessageCount + type: 0 + i18n: + zh-CN: 邮件总数 + en-US: Total message count + ja-JP: メッセージ総数 + - field: recentMessageCount + type: 0 + i18n: + zh-CN: 最近收到邮件总数 + en-US: Recent message count + ja-JP: 最新のメッセージ総数 + - field: unseenMessageCount + type: 0 + i18n: + zh-CN: 未读邮件总数 + en-US: Unread message count + ja-JP: 未読のメッセージ総数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - 其他文件夹/QQ邮件订阅TotalMessageCount + - 其他文件夹/QQ邮件订阅RecentMessageCount + - 其他文件夹/QQ邮件订阅UnseenMessageCount + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + calculates: + - allMessageCount = 其他文件夹/QQ邮件订阅TotalMessageCount + - recentMessageCount = 其他文件夹/QQ邮件订阅RecentMessageCount + - unseenMessageCount = 其他文件夹/QQ邮件订阅UnseenMessageCount + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: imap + # Specific collection configuration when protocol is http protocol + imap: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # email + email: ^_^email^_^ + # authorize code + authorize: ^_^authorize^_^ + # mailbox folder name + folderName: 其他文件夹/QQ邮件订阅 diff --git a/hertzbeat-manager/src/main/resources/define/app-rabbitmq.yml b/hertzbeat-manager/src/main/resources/define/app-rabbitmq.yml new file mode 100644 index 0000000..bd8c34e --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-rabbitmq.yml @@ -0,0 +1,663 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: mid +# The monitoring type eg: linux windows tomcat mysql aws... +app: rabbitmq +# The monitoring i18n name +name: + zh-CN: RabbitMQ消息系统 + en-US: RabbitMQ Message + ja-JP: RabbitMQメッセージシステム +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 RabbitMQ 消息中间件的运行状态,节点,队列等相关指标(overview、object_totals、nodes、queues)进行监测。<br><span class='help_module_span'>⚠️注意:HertzBeat 使用 RabbitMQ Management 的 Rest Api 对 RabbitMQ 进行指标数据采集,因此您的 RabbitMQ 环境需要开启 Management 插件,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/rabbitmq#%E7%9B%91%E6%8E%A7%E5%89%8D%E6%93%8D%E4%BD%9C'>点击查看开启步骤</a>。</span> + en-US: HertzBeat monitors RabbitMQ of the runtime status, nodes, queues and other metrics through message-oriented middleware. <br><span class='help_module_span'>⚠️Note:HertzBeat uses the Rest Api of RabbitMQ Management to collect metric data for RabbitMQ, so you need to enable Management plug-in component, <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/rabbitmq#%E7%9B%91%E6%8E%A7%E5%89%8D%E6%93%8D%E4%BD%9C'>Click here to view the specific steps.</a></span> + zh-TW: HertzBeat 對 RabbitMQ 消息中間件的運行狀態,節點,隊列等相關指標(overview、object_totals、nodes、queues)進行監測。<br><span class='help_module_span'>⚠️注意:HertzBeat 使用 RabbitMQ Management 的 Rest Api 對 RabbitMQ 進行指標數據采集,因此您的 RabbitMQ 環境需要開啓 Management 插件,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/rabbitmq#%E7%9B%91%E6%8E%A7%E5%89%8D%E6%93%8D%E4%BD%9C'>點擊查看開啓步驟</a>。</span> + ja-JP: HertzBeat は RabbitMQメッセージミドルウェアの実行状態、ノード、キューなどの関連指標(overview、object_totals、nodes、queues)を監視します。<br><span class='help_module_span'>⚠️注意:HertzBeatはRabbitMQ Managementの Rest Apiを使用してRabbitMQの指標データを収集するため、RabbitMQ環境でManagementプラグインを有効にする必要があります。<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/rabbitmq#%E7%9B%91%E6%8E%A7%E5%89%8D%E6%93%8D%E4%BD%9C'>有効化手順を確認するにはこちらをクリック</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/rabbitmq + en-US: https://hertzbeat.apache.org/docs/help/rabbitmq +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: HTTP 端口 + en-US: HTTP Port + ja-JP: HTTPポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + defaultValue: 15672 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL有効化 + # When the type is boolean, the front end displays the switch with a switch + type: boolean + # required-true or false + required: false + # field-param field key + - field: authType + # name-param field display i18n name + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + # When the type is radio radio radio and checkbox, option represents a list of optional values {name1: value1, name2: value2} + type: radio + # required-true or false + required: true + #When the type is radio radio radio and checkbox, option represents a list of optional values {name1: value1, name2: value2} + options: + - label: Basic Auth + value: Basic Auth + defaultValue: Basic Auth + # field-param field key + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # When the type is text, use limit to indicate the string limit size + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: true + +metrics: + # Note: The built-in monitoring metrics include (responseTime - response time) + - name: overview + i18n: + zh-CN: 概览 + en-US: Overview + ja-JP: 概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: product_version + type: 1 + i18n: + zh-CN: 产品版本 + en-US: Product Version + ja-JP: 製品バージョン + - field: product_name + type: 1 + i18n: + zh-CN: 产品名称 + en-US: Product Name + ja-JP: 製品名 + - field: rabbitmq_version + type: 1 + i18n: + zh-CN: RabbitMQ 版本 + en-US: RabbitMQ Version + ja-JP: RabbitMQバージョン + - field: management_version + type: 1 + i18n: + zh-CN: RabbitMQ Management 版本 + en-US: RabbitMQ Management Version + ja-JP: RabbitMQ Managementバージョン + - field: erlang_version + type: 1 + i18n: + zh-CN: Erlang 版本 + en-US: Erlang Version + ja-JP: Erlangバージョン + - field: cluster_name + type: 1 + i18n: + zh-CN: 集群名称 + en-US: Cluster Name + ja-JP: クラスター名 + - field: rates_mode + type: 1 + i18n: + zh-CN: 速率模式 + en-US: Rates Mode + ja-JP: レートモード + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # Host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url Indicates the path of the request interface + url: /api/overview + # Request mode GET POST PUT DELETE PATCH + method: GET + # Whether to enable SSL/TLS, i.e. whether it is HTTP or HTTPS, default is false. + ssl: ^_^ssl^_^ + # authentication + authorization: + # Authentication mode: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: default + + - name: object_totals + i18n: + zh-CN: 对象总数 + en-US: Object Totals + ja-JP: オブジェクト総数 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: channels + type: 0 + i18n: + zh-CN: 通道数 + en-US: Channels + ja-JP: チャネル数 + - field: connections + type: 0 + i18n: + zh-CN: 连接数 + en-US: Connections + ja-JP: 接続数 + - field: consumers + type: 0 + i18n: + zh-CN: 消费者数 + en-US: Consumers + ja-JP: コンシューマー数 + - field: exchanges + type: 0 + i18n: + zh-CN: 交换器数 + en-US: Exchanges + ja-JP: エクスチェンジ数 + - field: queues + type: 0 + i18n: + zh-CN: 队列数 + en-US: Queues + ja-JP: キュー数 + protocol: http + http: + # Host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url Indicates the path of the request interface + url: /api/overview + # Request mode GET POST PUT DELETE PATCH + method: GET + # Whether to enable SSL/TLS, i.e. whether it is HTTP or HTTPS, default is false. + ssl: ^_^ssl^_^ + # Authentication + authorization: + # Authentication mode: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$.object_totals' + + - name: nodes + i18n: + zh-CN: 节点数 + en-US: Nodes + ja-JP: ノード数 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: name + type: 1 + label: true + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: type + type: 1 + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: タイプ + - field: running + type: 1 + i18n: + zh-CN: 运行状态 + en-US: Running + ja-JP: 実行状態 + - field: os_pid + type: 1 + i18n: + zh-CN: 操作系统进程ID + en-US: OS PID + ja-JP: オーエスのプロセスID + - field: mem_limit + type: 0 + unit: MB + i18n: + zh-CN: 内存限制 + en-US: Memory Limit + ja-JP: メモリ制限 + - field: mem_used + type: 0 + unit: MB + i18n: + zh-CN: 已使用内存 + en-US: Memory Used + ja-JP: 使用したメモリ + - field: fd_total + type: 0 + i18n: + zh-CN: 文件描述符总数 + en-US: Total File Descriptors + ja-JP: ファイル記述子総数 + - field: fd_used + type: 0 + i18n: + zh-CN: 已使用文件描述符 + en-US: Used File Descriptors + ja-JP: 使用したファイル記述子 + - field: sockets_total + type: 0 + i18n: + zh-CN: 总套接字数 + en-US: Total Sockets + ja-JP: ソケット総数 + - field: sockets_used + type: 0 + i18n: + zh-CN: 已使用套接字数 + en-US: Used Sockets + ja-JP: 使用したソケット数 + - field: proc_total + type: 0 + i18n: + zh-CN: 进程总数 + en-US: Total Processes + ja-JP: プロセス総数 + - field: proc_used + type: 0 + i18n: + zh-CN: 已使用进程数 + en-US: Used Processes + ja-JP: 使用したプロセス数 + - field: disk_free_limit + type: 0 + unit: GB + i18n: + zh-CN: 磁盘可用空间限制 + en-US: Disk Free Limit + ja-JP: 利用可能なディスク容量の制限 + - field: disk_free + type: 0 + unit: GB + i18n: + zh-CN: 可用磁盘空间 + en-US: Disk Free + ja-JP: 利用可能なディスク容量 + - field: gc_num + type: 0 + i18n: + zh-CN: 垃圾回收次数 + en-US: GC Count + ja-JP: GC回数 + - field: gc_bytes_reclaimed + type: 0 + unit: MB + i18n: + zh-CN: 回收的垃圾字节数 + en-US: GC Bytes Reclaimed + ja-JP: GCによって解放されたバイト数 + - field: context_switches + type: 0 + i18n: + zh-CN: 上下文切换次数 + en-US: Context Switches + ja-JP: コンテキストスイッチ回数 + - field: io_read_count + type: 0 + i18n: + zh-CN: 读取次数 + en-US: IO Read Count + ja-JP: IO読み取り回数 + - field: io_read_bytes + type: 0 + unit: KB + i18n: + zh-CN: 读取字节数 + en-US: IO Read Bytes + ja-JP: IO読み取りバイト数 + - field: io_read_avg_time + type: 0 + unit: ms + i18n: + zh-CN: 平均读取时间 + en-US: IO Read Average Time + ja-JP: IO読み取り平均時間 + - field: io_write_count + type: 0 + i18n: + zh-CN: 写入次数 + en-US: IO Write Count + ja-JP: IO書き込み回数 + - field: io_write_bytes + type: 0 + unit: KB + i18n: + zh-CN: 写入字节数 + en-US: IO Write Bytes + ja-JP: IO書き込みバイト数 + - field: io_write_avg_time + type: 0 + unit: ms + i18n: + zh-CN: 平均写入时间 + en-US: IO Write Average Time + ja-JP: IO書き込み平均時間 + - field: io_seek_count + type: 0 + i18n: + zh-CN: 寻址次数 + en-US: IO Seek Count + ja-JP: IOシーク回数 + - field: io_seek_avg_time + type: 0 + unit: ms + i18n: + zh-CN: 平均寻址时间 + en-US: IO Seek Average Time + ja-JP: IOシーク平均時間 + - field: io_sync_count + type: 0 + i18n: + zh-CN: 同步次数 + en-US: IO Sync Count + ja-JP: IO同期回数 + - field: io_sync_avg_time + type: 0 + unit: ms + i18n: + zh-CN: 平均同步时间 + en-US: IO Sync Average Time + ja-JP: IO同期平均時間 + - field: connection_created + type: 0 + i18n: + zh-CN: 创建的连接数 + en-US: Connections Created + ja-JP: 作成された接続数 + - field: connection_closed + type: 0 + i18n: + zh-CN: 关闭的连接数 + en-US: Connections Closed + ja-JP: 閉じられた接続数 + - field: channel_created + type: 0 + i18n: + zh-CN: 创建的通道数 + en-US: Channels Created + ja-JP: 作成されたチャネル数 + - field: channel_closed + type: 0 + i18n: + zh-CN: 关闭的通道数 + en-US: Channels Closed + ja-JP: 閉じられたチャネル数 + - field: queue_declared + type: 0 + i18n: + zh-CN: 声明的队列数 + en-US: Queues Declared + ja-JP: 宣言されたキュー数 + - field: queue_created + type: 0 + i18n: + zh-CN: 创建的队列数 + en-US: Queues Created + ja-JP: 作成されたキュー数 + - field: queue_deleted + type: 0 + i18n: + zh-CN: 删除的队列数 + en-US: Queues Deleted + ja-JP: 削除されたキュー数 + units: + - mem_limit=B->MB + - mem_used=B->MB + - disk_free_limit=B->GB + - disk_free=B->GB + - gc_bytes_reclaimed=B->MB + - io_read_bytes=B->KB + - io_write_bytes=B->KB + protocol: http + http: + # Host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url Indicates the path of the request interface + url: /api/nodes + # request mode: GET POST PUT DELETE PATCH + method: GET + # Whether to enable SSL/TLS, i.e. whether it is HTTP or HTTPS, default is false. + ssl: ^_^ssl^_^ + # Authentication + authorization: + # Authentication mode: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + # response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$.*' + + - name: queues + i18n: + zh-CN: 队列信息 + en-US: Queue info + ja-JP: キュー情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: name + type: 1 + label: true + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: node + type: 1 + i18n: + zh-CN: 节点 + en-US: Node + ja-JP: ノード + - field: state + type: 1 + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + - field: type + type: 1 + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: タイプ + - field: vhost + type: 1 + i18n: + zh-CN: 虚拟主机 + en-US: Vhost + ja-JP: 仮想ホスト + - field: auto_delete + type: 1 + i18n: + zh-CN: 自动删除 + en-US: Auto Delete + ja-JP: 自動削除 + - field: policy + type: 1 + i18n: + zh-CN: 策略 + en-US: Policy + ja-JP: ポリシー + - field: consumers + type: 0 + i18n: + zh-CN: 消费者数 + en-US: Consumers + ja-JP: コンシューマー数 + - field: memory + type: 0 + unit: B + i18n: + zh-CN: 内存占用 + en-US: Memory + ja-JP: メモリ使用量 + - field: messages_ready + type: 0 + i18n: + zh-CN: 就绪消息数 + en-US: Messages Ready + ja-JP: 準備完了メッセージ数 + - field: messages_unacknowledged + type: 0 + i18n: + zh-CN: 未确认消息数 + en-US: Messages Unacknowledged + ja-JP: 未確認メッセージ数 + - field: messages + type: 0 + i18n: + zh-CN: 消息总数 + en-US: Messages + ja-JP: メッセージ総数 + - field: messages_ready_ram + type: 0 + i18n: + zh-CN: 就绪消息占用内存 + en-US: Messages Ready RAM + ja-JP: 準備完了メッセージRAM使用量 + - field: messages_persistent + type: 0 + i18n: + zh-CN: 持久化消息数 + en-US: Persistent Messages + ja-JP: 永続化されたメッセージの数 + - field: message_bytes + type: 0 + unit: B + i18n: + zh-CN: 消息字节数 + en-US: Message Bytes + ja-JP: メッセージバイト数 + - field: message_bytes_ready + type: 0 + unit: B + i18n: + zh-CN: 就绪消息字节数 + en-US: Message Bytes Ready + ja-JP: 準備完了メッセージバイト数 + - field: message_bytes_unacknowledged + type: 0 + unit: B + i18n: + zh-CN: 未确认消息字节数 + en-US: Message Bytes Unacknowledged + ja-JP: 未確認メッセージバイト数 + - field: message_bytes_ram + type: 0 + unit: B + i18n: + zh-CN: 消息占用内存字节数 + en-US: Message Bytes RAM + ja-JP: メッセージ使用メモリバイト数 + - field: message_bytes_persistent + type: 0 + unit: B + i18n: + zh-CN: 持久化消息字节数 + en-US: Persistent Message Bytes + ja-JP: 永続化されたメッセージのバイト数 + protocol: http + http: + # Host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url Indicates the path of the request interface + url: /api/queues + # request mode GET POST PUT DELETE PATCH + method: GET + # Whether to enable SSL/TLS, i.e. whether it is HTTP or HTTPS, default is false. + ssl: ^_^ssl^_^ + # Authentication + authorization: + # Authentication mode: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + # response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$.*' + diff --git a/hertzbeat-manager/src/main/resources/define/app-redfish.yml b/hertzbeat-manager/src/main/resources/define/app-redfish.yml new file mode 100644 index 0000000..1f6987a --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-redfish.yml @@ -0,0 +1,274 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: server +# The monitoring type eg: linux windows tomcat mysql aws... +app: redfish +# The monitoring i18n name +name: + zh-CN: Redfish + en-US: Redfish + ja-JP: Redfish +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对支持 Redfish 服务的服务器进行测量监控。<br>您可以点击 “<i>新建 Redfish</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitoring servers supporting Redfish services. You could click the "<i>New Redfish</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對支援 Redfish 服務的伺服器進行測量監控。<br>您可以點擊“<i>Redfish</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は Redfishをサポートするサーバーの一般的なメトリクスを監視します。<br>「<i>新規 FTPサーバー</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/redfish + en-US: https://hertzbeat.apache.org/docs/help/redfish +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 443 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false +# collect metrics config list +metrics: + # metrics - cpu + - name: Chassis + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: id + type: 1 + i18n: + zh-CN: 机箱唯一标识符 + en-US: Chassis Unique Identifier + ja-JP: シャーシID + - field: name + type: 1 + i18n: + zh-CN: 机箱名称 + en-US: Chassis Name + ja-JP: シャーシ名 + - field: type + type: 1 + i18n: + zh-CN: 机箱类型 + en-US: Chassis Type + ja-JP: シャーシタイプ + - field: state + type: 1 + i18n: + zh-CN: 机箱状态 + en-US: Chassis State + ja-JP: シャーシ状態 + - field: health + type: 1 + i18n: + zh-CN: 机箱健康状态 + en-US: Chassis Health + ja-JP: シャーシ健全状態 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redfish + # the config content when protocol is redfish + redfish: + # redfish host: ipv4 ipv6 domain + host: ^_^host^_^ + # redfish port + port: ^_^port^_^ + # redfish username + username: ^_^username^_^ + # redfish password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + jsonPath: + - $.['@odata.id'] + - $.Name + - $.ChassisType + - $.Status.State + - $.Status.Health + + - name: Battery + priority: 1 + fields: + - field: id + type: 1 + i18n: + zh-CN: 电池唯一标识符 + en-US: Battery Unique Identifier + ja-JP: 電池ID + - field: name + type: 1 + i18n: + zh-CN: 电池名称 + en-US: Battery Name + ja-JP: 電池名 + - field: state + type: 1 + i18n: + zh-CN: 电池状态 + en-US: Battery State + ja-JP: 電池状態 + - field: health + type: 1 + i18n: + zh-CN: 电池健康状态 + en-US: Battery Health + ja-JP: 電池健全状態 + - field: charge_status + type: 1 + i18n: + zh-CN: 电池充电状态 + en-US: Battery Charge Status + ja-JP: 電池充電状況 + protocol: redfish + redfish: + # redfish host: ipv4 ipv6 domain + host: ^_^host^_^ + # redfish port + port: ^_^port^_^ + # redfish username + username: ^_^username^_^ + # redfish password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + jsonPath: + - $.['@odata.id'] + - $.Name + - $.Status.State + - $.Status.Health + - $.ChargeState + + - name: Fan + priority: 2 + fields: + - field: id + type: 1 + i18n: + zh-CN: 风扇唯一标识符 + en-US: Fan Unique Identifier + ja-JP: ファンID + - field: name + type: 1 + i18n: + zh-CN: 风扇名称 + en-US: Fan Name + ja-JP: ファン名 + - field: state + type: 1 + i18n: + zh-CN: 风扇状态 + en-US: Fan State + ja-JP: ファン状態 + - field: health + type: 1 + i18n: + zh-CN: 风扇健康状态 + en-US: Fan Health + ja-JP: ファン健全状態 + - field: temperature + type: 0 + i18n: + zh-CN: 传感器温度 + en-US: Sensor Temperature + ja-JP: センサー温度 + - field: speed + type: 0 + i18n: + zh-CN: 风扇转速 + en-US: Fan Speed + ja-JP: ファンスピード + protocol: redfish + redfish: + # redfish host: ipv4 ipv6 domain + host: ^_^host^_^ + # redfish port + port: ^_^port^_^ + # redfish username + username: ^_^username^_^ + # redfish password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + # redfish fan collection schema + schema: /redfish/v1/Chassis/{ChassisId}/ThermalSubsystem/Fans + jsonPath: + - $.['@odata.id'] + - $.Name + - $.Status.State + - $.Status.Health + - $.SpeedPercent.Reading + - $.SpeedPercent.SpeedRPM diff --git a/hertzbeat-manager/src/main/resources/define/app-redhat.yml b/hertzbeat-manager/src/main/resources/define/app-redhat.yml new file mode 100644 index 0000000..6e5e54a --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-redhat.yml @@ -0,0 +1,765 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: redhat +# The monitoring i18n name +name: + zh-CN: Red Hat + en-US: Red Hat + ja-JP: Red Hat +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 Red Hat 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Red Hat</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors Red Hat operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New Red Hat</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 Red Hat 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Red Hat</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> Red Hatオーエスの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Red Hat</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/redhat + en-US: https://hertzbeat.apache.org/docs/help/redhat +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: システムバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: (uname -r ; hostname ; uptime | awk -F "," '{print $1}' | sed "s/ //g") | sed ":a;N;s/\n/^/g;ta" | awk -F '^' 'BEGIN{print "version hostname uptime"} {print $1, $2, $3}' + # ssh response data parse type: oneRow, multiRow + parseType: multiRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100-idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: "LANG=C lscpu | awk -F: '$1==\"Model name\" {print $2}';awk '/processor/{core++} END{print core}' /proc/cpuinfo;uptime | sed 's/,/ /g' | awk '{for(i=NF-2;i<=NF;i++)print $i }' | xargs;vmstat 1 1 | awk 'NR==3{print $11}';vmstat 1 1 | awk 'NR==3{print $12}';vmstat 1 2 | awk 'NR==4{print $15}'" + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: free -m | awk 'BEGIN{print "total used free buff_cache available"} NR==2{print $2,$3,$4,$6,$7}' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + en-US: Partition Num + ja-JP: パーティション + - field: block_write + type: 0 + i18n: + zh-CN: 写磁盘块数 + en-US: Block Write + ja-JP: 書き込みディスクブロック数 + - field: block_read + type: 0 + i18n: + zh-CN: 读磁盘块数 + en-US: Block Read + ja-JP: 読み取りブロック数 + - field: write_rate + type: 0 + unit: iops + i18n: + zh-CN: 磁盘写速率 + en-US: Write Rate + ja-JP: ディスク書き込み速度 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: vmstat -D | awk 'NR==1{print $1}';vmstat -D | awk 'NR==2{print $1}';vmstat 1 1 | awk 'NR==3{print $10}';vmstat 1 1 | awk 'NR==3{print $9}';vmstat 1 1 | awk 'NR==3{print $16}' + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: cat /proc/net/dev | tail -n +3 | awk 'BEGIN{ print "interface_name receive_bytes transmit_bytes"} {print $1,$2,$10}' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -mP | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-redis.yml b/hertzbeat-manager/src/main/resources/define/app-redis.yml new file mode 100644 index 0000000..a7d442c --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-redis.yml @@ -0,0 +1,1658 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: cache +# The monitoring type eg: linux windows tomcat mysql aws... +app: redis +# The monitoring i18n name +name: + zh-CN: Redis数据库 + en-US: Redis + ja-JP: Redisデータベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 REDIS 数据库的通用性能指标进行采集监控(server、clients、memory、persistence、stats、replication、cpu、errorstats、cluster、commandstats),支持版本为 REDIS1.0+。<br>您可以点击“<i>新建 REDIS 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat monitors REDIS database of general performance metrics such as memory, persistence, replication and so on. The versions we support is REDIS1.0+. <br>You could click the "<i>New REDIS</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: HertzBeat 對 REDIS 數據庫的通用性能指標進行采集監控(server、clients、memory、persistence、stats、replication、cpu、errorstats、cluster、commandstats),支持版本爲 REDIS1.0+。<br>您可以點擊“<i>新建 REDIS 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は REDIS データベースの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 REDIS データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/redis + en-US: https://hertzbeat.apache.org/docs/help/redis +# Input params define for monitoring(render web ui by +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 6379 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间 + en-US: Timeout + ja-JP: タイムアウト + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value + defaultValue: 3000 + # field-param field key + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + # field-param field key + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + - field: enableSshTunnel + name: + zh-CN: 是否启用SSH隧道 + en-US: Enable SSH Tunnel + ja-JP: SSHトンネルの有効化 + type: boolean + required: true + hide: true + - field: sshHost + name: + zh-CN: SSH Host + en-US: SSH Host + ja-JP: SSHホスト + type: text + required: false + placeholder: 'When Enable SSH Tunnel' + hide: true + - field: sshPort + name: + zh-CN: SSH端口 + en-US: SSH Port + ja-JP: SSHポート + type: number + range: '[0,65535]' + required: false + defaultValue: 22 + placeholder: 'When Enable SSH tunnel' + hide: true + - field: sshTimeout + name: + zh-CN: SSH超时时间(ms) + en-US: SSH Timeout(ms) + ja-JP: SSHタイムアウト(ms) + type: number + required: false + range: '[400,200000]' + defaultValue: 6000 + hide: true + - field: sshUsername + name: + zh-CN: SSH用户名 + en-US: SSH Username + ja-JP: SSHユーザー名 + type: text + required: false + placeholder: 'When Enable SSH tunnel' + hide: true + - field: sshPassword + name: + zh-CN: SSH密码 + en-US: SSH Password + ja-JP: SSHパスワード + type: password + required: false + hide: true + - field: sshShareConnection + name: + zh-CN: 是否共享SSH连接 + en-US: Share SSH Connection + ja-JP: SSH接続共有 + type: boolean + required: true + defaultValue: true + hide: true + - field: sshPrivateKey + name: + zh-CN: SSH私钥 + en-US: SSH PrivateKey + ja-JP: SSH秘密鍵 + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + required: false + hide: true + - field: sshPrivateKeyPassphrase + name: + zh-CN: SSH密钥短语 + en-US: SSH PrivateKey PassPhrase + ja-JP: SSH秘密鍵フレーズ + type: password + required: false + hide: true +# collect metrics config list +metrics: + # metrics - server + - name: server + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + i18n: + zh-CN: 服务器信息 + en-US: Server + ja-JP: サーバー情報 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: ID + en-US: Identity + ja-JP: ID + - field: redis_version + type: 1 + i18n: + zh-CN: Redis服务版本 + en-US: Redis Version + ja-JP: Redisバージョン + - field: redis_git_sha1 + type: 0 + i18n: + zh-CN: Git SHA1 + en-US: Git SHA1 + ja-JP: Git SHA1 + - field: redis_git_dirty + type: 0 + i18n: + zh-CN: Redis服务器代码是否有改动 + en-US: Git Dirty Flag + ja-JP: RedisサーバーのGitリポジトリ状態 + - field: redis_build_id + type: 1 + i18n: + zh-CN: Build Id + en-US: Build Id + ja-JP: ビルド Id + - field: redis_mode + type: 1 + i18n: + zh-CN: 运行模式 + en-US: Server Mode + ja-JP: サーバーモード + - field: os + type: 1 + i18n: + zh-CN: 操作系统 + en-US: Operating System + ja-JP: オーエス + - field: arch_bits + type: 0 + i18n: + zh-CN: 架构 + en-US: Architecture Bits + ja-JP: アーキテクチャ + - field: multiplexing_api + type: 1 + i18n: + zh-CN: IO多路复用器API + en-US: Multiplexing API + ja-JP: IO多重化API + - field: atomicvar_api + type: 1 + i18n: + zh-CN: 原子操作处理API + en-US: Atomicvar API + ja-JP: 原子操作API + - field: gcc_version + type: 1 + i18n: + zh-CN: GCC版本 + en-US: GCC Version + ja-JP: GCC バージョン + - field: process_id + type: 0 + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: process_supervised + type: 1 + i18n: + zh-CN: 进程监督机制管理Redis的方式 + en-US: Process Supervised + ja-JP: プロセスの監視方法 + - field: run_id + type: 1 + i18n: + zh-CN: Run ID + en-US: Run ID + ja-JP: Run ID + - field: tcp_port + type: 0 + i18n: + zh-CN: TCP/IP监听端口 + en-US: TCP Port + ja-JP: TCP ポート + - field: server_time_usec + type: 0 + i18n: + zh-CN: 服务器时间戳 + en-US: Server Time Usec + ja-JP: サーバーのタイムスタンプ + - field: uptime_in_seconds + type: 0 + i18n: + zh-CN: 运行时长(秒) + en-US: Uptime(Seconds) + ja-JP: アップタイム(秒) + - field: uptime_in_days + type: 0 + i18n: + zh-CN: 运行时长(天) + en-US: Uptime(Days) + ja-JP: アップタイム(日) + - field: hz + type: 0 + i18n: + zh-CN: 事件循环频率 + en-US: hz + ja-JP: イベントの実行頻度 + - field: configured_hz + type: 0 + i18n: + zh-CN: 配置的事件循环频率 + en-US: Configured hz + ja-JP: 設定されたイベントの実行頻度 + - field: lru_clock + type: 0 + i18n: + zh-CN: LRU时钟 + en-US: LRU Clock + ja-JP: LRU クロック + - field: executable + type: 1 + i18n: + zh-CN: 服务器执行路径 + en-US: Server's Executable Path + ja-JP: サーバーの実行パス + - field: config_file + type: 1 + i18n: + zh-CN: 配置文件路径 + en-US: Config File Path + ja-JP: 設定されたサーバーの実行ファイル + - field: io_threads_active + type: 0 + i18n: + zh-CN: 活跃IO线程数 + en-US: Active IO Threads + ja-JP: 活動中のスレッド数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + # metrics - clients + - name: clients + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + i18n: + zh-CN: 客户端信息 + en-US: Clients + ja-JP: クライアント情報 + # collect metrics content + fields: + - field: connected_clients + type: 0 + i18n: + zh-CN: 已连接客户端数量 + en-US: Connected Clients + ja-JP: 現在接続されているクライアントの数 + - field: cluster_connections + type: 0 + i18n: + zh-CN: 集群客户端连接数 + en-US: Cluster Connections + ja-JP: クラスター内の接続数 + - field: maxclients + type: 0 + i18n: + zh-CN: 最大客户端连接数 + en-US: Max Clients + ja-JP: 最大クライアント数 + - field: client_recent_max_input_buffer + type: 0 + i18n: + zh-CN: 最近客户端的最大输入缓冲区大小 + en-US: Client Recent Max Input Buffer + ja-JP: クライアントの最近の最大入力バッファサイズ + - field: client_recent_max_output_buffer + type: 0 + i18n: + zh-CN: 最近客户端的最大输出缓冲区大小 + en-US: Client Recent Max Output Buffer + ja-JP: クライアントの最近の最大出力バッファサイズ + - field: blocked_clients + type: 0 + i18n: + zh-CN: 阻塞客户端数量 + en-US: Blocked Clients + ja-JP: ブロックされたクライアント数 + - field: tracking_clients + type: 0 + i18n: + zh-CN: 正在追踪数据的客户端数量 + en-US: Tracking Clients + ja-JP: トラッキングを使用するクライアントの数 + - field: clients_in_timeout_table + type: 0 + i18n: + zh-CN: 超时队列中的客户端数量 + en-US: Clients In Timeout Table + ja-JP: タイムアウトテーブル内のクライアント数 + protocol: redis + redis: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + timeout: ^_^timeout^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + # metrics - memory + - name: memory + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + i18n: + zh-CN: 内存信息 + en-US: Memory + ja-JP: メモリ情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: used_memory + type: 0 + i18n: + zh-CN: 已使用内存(字节) + en-US: Used Memory + ja-JP: 使用したメモリ(バイト) + - field: used_memory_human + type: 0 + unit: MB + i18n: + zh-CN: 已使用内存 + en-US: Used Memory Human + ja-JP: 使用したメモリ + - field: used_memory_rss + type: 0 + i18n: + zh-CN: 已使用物理内存(字节) + en-US: Used Memory RSS + ja-JP: 使用した物理メモリ(バイト) + - field: used_memory_rss_human + type: 0 + unit: MB + i18n: + zh-CN: 已使用物理内存 + en-US: Used Memory RSS Human + ja-JP: 使用した物理メモリ + - field: used_memory_peak + type: 0 + i18n: + zh-CN: 内存使用最大峰值(字节) + en-US: Used Memory Peak + ja-JP: 使用したメモリの最大値(バイト) + - field: used_memory_peak_human + type: 0 + unit: MB + i18n: + zh-CN: 内存使用最大值 + en-US: Used Memory Peak Human + ja-JP: 使用したメモリの最大値 + - field: used_memory_peak_perc + type: 0 + unit: '%' + i18n: + zh-CN: 最大内存使用率 + en-US: Used Memory Peak Perc + ja-JP: メモリの最大使用率 + - field: used_memory_overhead + type: 0 + i18n: + zh-CN: 管理数据结构的额外内存使用量 + en-US: Used Memory Overhead + ja-JP: オーバーヘッドのメモリ使用量 + - field: used_memory_startup + type: 0 + i18n: + zh-CN: Redis服务器启动时使用的内存 + en-US: Used Memory Startup + ja-JP: 起動時に使用したメモリ量 + - field: used_memory_dataset + type: 0 + i18n: + zh-CN: 存储数据占用的内存 + en-US: Used Memory Dataset + ja-JP: データセットが使用中のメモリ量 + - field: used_memory_dataset_perc + type: 0 + unit: '%' + i18n: + zh-CN: 存储数据占用的内存比率 + en-US: Used Memory Dataset Perc + ja-JP: データセットが使用中のメモリ率 + - field: allocator_allocated + type: 0 + i18n: + zh-CN: 内存分配器分配的内存 + en-US: Allocator Allocated + ja-JP: 実際に割り当てられたメモリ量 + - field: allocator_active + type: 0 + i18n: + zh-CN: 内存分配器激活的内存 + en-US: Allocator Active + ja-JP: メモリ割り当て器がオーエスに要求したメモリ量 + - field: allocator_resident + type: 0 + i18n: + zh-CN: 内存分配器加载的内存 + en-US: Allocator Resident + ja-JP: メモリ割り当て器が実際に使用中のメモリ量 + - field: total_system_memory + type: 0 + i18n: + zh-CN: 总系统内存容量(字节) + en-US: Total System Memory + ja-JP: 使用可能なシステムメモリの合計(バイト) + - field: total_system_memory_human + type: 0 + unit: GB + i18n: + zh-CN: 总系统内存容量 + en-US: Total System Memory Human + ja-JP: 使用可能なシステムメモリの合計 + - field: used_memory_lua + type: 0 + i18n: + zh-CN: LUA脚本占用的内存(字节) + en-US: Used Memory LUA + ja-JP: Luaスクリプトが使用中のメモリの量(バイト) + - field: used_memory_lua_human + type: 0 + unit: KB + i18n: + zh-CN: LUA脚本占用的内存 + en-US: Used Memory LUA Human + ja-JP: Luaスクリプトが使用しているメモリの量 + - field: used_memory_scripts + type: 0 + i18n: + zh-CN: Redis缓存的LUA脚本大小(字节) + en-US: Used Memory Scripts + ja-JP: キャッシュされたLuaスクリプトのメモリ量(バイト) + - field: used_memory_scripts_human + type: 0 + unit: KB + i18n: + zh-CN: Redis缓存的LUA脚本大小 + en-US: Used Memory Scripts Human + ja-JP: キャッシュされているLuaスクリプトのメモリ量 + - field: number_of_cached_scripts + type: 0 + i18n: + zh-CN: Redis缓存的LUA脚本数量 + en-US: Number Of Cached Scripts + ja-JP: キャッシュされたLuaスクリプトの数 + - field: maxmemory + type: 0 + i18n: + zh-CN: 最大内存限制(字节) + en-US: Max Memory + ja-JP: メモリの最大容量(バイト) + - field: maxmemory_human + type: 0 + unit: MB + i18n: + zh-CN: 最大内存限制 + en-US: Max Memory Human + ja-JP: メモリの最大容量 + - field: maxmemory_policy + type: 1 + i18n: + zh-CN: 内存淘汰策略 + en-US: Max Memory Policy + ja-JP: キーの削除ポリシー + - field: allocator_frag_ratio + type: 0 + i18n: + zh-CN: 内存分配器中的内存碎片占比 + en-US: Allocator Frag Ratio + ja-JP: メモリ割り当て器のメモリ断片化の比率 + - field: allocator_frag_bytes + type: 0 + i18n: + zh-CN: 内存分配器中的内存碎片大小 + en-US: Allocator Frag Bytes + ja-JP: メモリ割り当て器のメモリ断片化のバイト数 + - field: allocator_rss_ratio + type: 0 + i18n: + zh-CN: 内存分配器分配的内存占比 + en-US: Allocator RSS Ratio + ja-JP: メモリ割り当て器の物理メモリの比率 + - field: allocator_rss_bytes + type: 0 + i18n: + zh-CN: 内存分配器分配的内存大小 + en-US: Allocator RSS Bytes + ja-JP: メモリ割り当て器の実際に使用中の物理メモリのバイト数 + - field: rss_overhead_ratio + type: 0 + i18n: + zh-CN: 实际内存占比 + en-US: RSS Overhead Ratio + ja-JP: 物理メモリオーバーヘッドの比率 + - field: rss_overhead_bytes + type: 0 + i18n: + zh-CN: 实际内存大小 + en-US: RSS Overhead Bytes + ja-JP: 物理メモリオーバーヘッドのバイト数 + - field: mem_fragmentation_ratio + type: 0 + i18n: + zh-CN: 内存碎片率 + en-US: Mem Fragmentation Ratio + ja-JP: メモリ断片化の比率 + - field: mem_fragmentation_bytes + type: 0 + i18n: + zh-CN: 内存碎片大小 + en-US: Mem Fragmentation Bytes + ja-JP: メモリ断片化によって発生したバイト数 + - field: mem_not_counted_for_evict + type: 0 + i18n: + zh-CN: 未计入最大内存限制的内存 + en-US: Mem Not Counted For Evict + ja-JP: 強制抽出の対象ではないメモリの量 + - field: mem_replication_backlog + type: 0 + i18n: + zh-CN: 主从同步缓冲区占用的内存(字节) + en-US: Mem Replication Backlog + ja-JP: 複製バックログが使用中のメモリの量 + - field: mem_clients_slaves + type: 0 + i18n: + zh-CN: 从节点占用的内存 + en-US: Mem Clients Slaves + ja-JP: スレーブクライアントが使用中のメモリの量 + - field: mem_clients_normal + type: 0 + i18n: + zh-CN: 客户端占用的内存 + en-US: Mem Clients Normal + ja-JP: クライアントが使用中のメモリの量 + - field: mem_aof_buffer + type: 0 + i18n: + zh-CN: AOF缓冲区占用的内存 + en-US: Mem AOF Buffer + ja-JP: AOFバッファで使用中のメモリ量 + - field: mem_allocator + type: 1 + i18n: + zh-CN: 内存分配器 + en-US: Mem Allocator + ja-JP: メモリ割り当て器 + - field: active_defrag_running + type: 0 + i18n: + zh-CN: 是否正在整理内存 + en-US: Active Defrag Running + ja-JP: アクティブなデフラグ操作のステータス + - field: lazyfree_pending_objects + type: 0 + i18n: + zh-CN: 待处理的惰性删除对象数量 + en-US: Lazyfree Pending Objects + ja-JP: Lazyfree作業が保留中のオブジェクトの数 + - field: lazyfreed_objects + type: 0 + i18n: + zh-CN: 已经执行的惰性删除对象数量 + en-US: Lazyfreed Objects + ja-JP: Lazyfre作業で解除されたオブジェクトの数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + # metrics - persistence + - name: persistence + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + i18n: + zh-CN: 持久化信息 + en-US: Persistence + ja-JP: 永続化情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: loading + type: 0 + i18n: + zh-CN: 是否正在加载持久化文件 + en-US: Loading + ja-JP: データロード有無 + - field: current_cow_size + type: 0 + i18n: + zh-CN: COW区域内存大小 + en-US: Current COW Size + ja-JP: 現在COWのサイズ + - field: current_cow_size_age + type: 0 + i18n: + zh-CN: COW区域内存使用时间 + en-US: Current COW Size Age + ja-JP: 現在COWのサイズスパン + - field: current_fork_perc + type: 0 + i18n: + zh-CN: COW区域内存使用率 + en-US: Current Fork Perc + ja-JP: 現在のフォーク作業の進行率 + - field: current_save_keys_processed + type: 0 + i18n: + zh-CN: 正在处理的保存键数量 + en-US: Current Save Keys Processed + ja-JP: 現在の保存作業で処理されたキーの数 + - field: current_save_keys_total + type: 0 + i18n: + zh-CN: 保存键总数量 + en-US: Current Save Keys Total + ja-JP: 現在の保存作業で処理するキー数の合計 + - field: rdb_changes_since_last_save + type: 0 + i18n: + zh-CN: 自最近一次RDB后的数据改动条数 + en-US: RDB Changes Since Last Save + ja-JP: RDBファイルを最後に保存して以来、入力されたデータ件数 + - field: rdb_bgsave_in_progress + type: 0 + i18n: + zh-CN: 是否正在进行RDB的bgsave命令 + en-US: RDB bgsave In Progress + ja-JP: バックグラウンドRDBの保存作業が進行中 + - field: rdb_last_save_time + type: 0 + i18n: + zh-CN: 最近一次bgsave命令执行时间 + en-US: RDB Last Save Time + ja-JP: 最後のRDB保存時刻 + - field: rdb_last_bgsave_status + type: 1 + i18n: + zh-CN: 最近一次bgsave命令执行状态 + en-US: RDB Last bgsave Status + ja-JP: 最後のRDB保存ステータス + - field: rdb_last_bgsave_time_sec + type: 0 + i18n: + zh-CN: 最近一次bgsave命令执行时间(秒) + en-US: RDB Last bgsave Time Sec + ja-JP: 最後のバックグラウンドRDB保存にかかった時間(秒) + - field: rdb_current_bgsave_time_sec + type: 0 + i18n: + zh-CN: 当前bgsave命令执行时间(秒) + en-US: RDB Current bgsave Time Sec + ja-JP: 現在バックグラウンドでRDBを保存している場合、保存開始から現在までの所要時間(秒) + - field: rdb_last_cow_size + type: 0 + i18n: + zh-CN: RDB最近一次COW区域内存大小 + en-US: RDB Last COW Size + ja-JP: 最後のRDB保存時点のCOWのサイズ + - field: aof_enabled + type: 0 + i18n: + zh-CN: 是否开启了AOF + en-US: AOF Enabled + ja-JP: AOFの活性化状態の有無 + - field: aof_rewrite_in_progress + type: 0 + i18n: + zh-CN: 是否正在进行AOF的rewrite命令 + en-US: AOF rewrite In Progress + ja-JP: バックグラウンドAOFの保存作業が進行中 + - field: aof_rewrite_scheduled + type: 0 + i18n: + zh-CN: 是否在RDB的bgsave结束后执行AOF的rewirte + en-US: AOF rewrite Scheduled + ja-JP: AOF rewriteを実行する予定 + - field: aof_last_rewrite_time_sec + type: 0 + i18n: + zh-CN: 最近一次AOF的rewrite命令执行时间(秒) + en-US: AOF Last rewrite Time Sec + ja-JP: 最後のAOF rewriteにかかった時間(秒) + - field: aof_current_rewrite_time_sec + type: 0 + i18n: + zh-CN: 当前rewrite命令执行时间(秒) + en-US: AOF Current rewrite Time Sec + ja-JP: 現在AOF rewriteにかかった時間(秒) + - field: aof_last_bgrewrite_status + type: 1 + i18n: + zh-CN: 最近一次AOF的bgrewrite命令执行状态 + en-US: AOF Last bgrewrite Status + ja-JP: 最後のAOF rewriteのステータス + - field: aof_last_write_status + type: 1 + i18n: + zh-CN: 最近一次AOF写磁盘结果 + en-US: AOF Last Write Status + ja-JP: 最後のAOF書き込みのステータス + - field: aof_last_cow_size + type: 0 + i18n: + zh-CN: AOF最近一次COW区域内存大小 + en-US: AOF Last Cow Size + ja-JP: 最後のAOF rewrite時点のCOWのサイズ + - field: module_fork_in_progress + type: 0 + i18n: + zh-CN: 是否正在进行fork操作 + en-US: Module Fork In Progress + ja-JP: モジュールフォークの進行有無 + - field: module_fork_last_cow_size + type: 0 + i18n: + zh-CN: 最近一次执行fork操作的COW区域内存大小 + en-US: Module Fork Last Cow Size + ja-JP: 最後のモジュールフォーク時点のCOWのサイズ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + # metrics - stats + - name: stats + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 4 + i18n: + zh-CN: 全局统计信息 + en-US: Stats + ja-JP: 統計情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: total_connections_received + type: 0 + i18n: + zh-CN: 已接受的总连接数 + en-US: Total Connections Received + ja-JP: サーバーの起動後に受信された合計接続数 + - field: total_commands_processed + type: 0 + i18n: + zh-CN: 执行过的命令总数 + en-US: Total Commands Processed + ja-JP: サーバーの起動後に処理した総コマンド数 + - field: instantaneous_ops_per_sec + type: 0 + i18n: + zh-CN: 命令处理条数/秒 + en-US: Instantaneous Ops Per Sec + ja-JP: 1秒当たりに処理されたコマンドの数 + - field: total_net_input_bytes + type: 0 + i18n: + zh-CN: 输入总网络流量(字节) + en-US: Total Net Input Bytes + ja-JP: 受信されたネットワークバイトの合計数 + - field: total_net_output_bytes + type: 0 + i18n: + zh-CN: 输出总网络流量(字节) + en-US: Total Net Output Bytes + ja-JP: 転送されたネットワークバイトの合計数 + - field: instantaneous_input_kbps + type: 0 + i18n: + zh-CN: 输入字节数/秒 + en-US: Instantaneous Input Kbps + ja-JP: 瞬間入力速度(kbps) + - field: instantaneous_output_kbps + type: 0 + i18n: + zh-CN: 输出字节数/秒 + en-US: Instantaneous Output Kbps + ja-JP: 瞬間出力速度(kbps) + - field: rejected_connections + type: 0 + i18n: + zh-CN: 拒绝连接数 + en-US: Rejected Connections + ja-JP: maxclients制限で拒否された接続数 + - field: sync_full + type: 0 + i18n: + zh-CN: 主从完全同步成功次数 + en-US: Sync Full + ja-JP: 完全同期の回数 + - field: sync_partial_ok + type: 0 + i18n: + zh-CN: 主从部分同步成功次数 + en-US: Sync Partial OK + ja-JP: 部分同期の回数 + - field: sync_partial_err + type: 0 + i18n: + zh-CN: 主从部分同步失败次数 + en-US: Sync Partial Error + ja-JP: 部分同期のエラー回数 + - field: expired_keys + type: 0 + i18n: + zh-CN: 过期key数量 + en-US: Expired Keys + ja-JP: expireコマンドで削除されたキーの数 + - field: expired_stale_perc + type: 0 + i18n: + zh-CN: 过期key占比 + en-US: Expired Stale Perc + ja-JP: 期限切れのステイルキーの比率 + - field: expired_time_cap_reached_count + type: 0 + i18n: + zh-CN: 过期key清理操作被限制次数 + en-US: Expired Time Cap Reached Count + ja-JP: 満了時間の制限に達した回数 + - field: expire_cycle_cpu_milliseconds + type: 0 + i18n: + zh-CN: 清理过期key消耗的cpu时间(毫秒) + en-US: Expire Cycle CPU Milliseconds + ja-JP: 満了サイクルでかかったCPU時間(ms) + - field: evicted_keys + type: 0 + i18n: + zh-CN: 淘汰key数量 + en-US: Evicted Keys + ja-JP: メモリ不足で追放されたキーの数 + - field: keyspace_hits + type: 0 + i18n: + zh-CN: key命中成功次数 + en-US: Keyspace Hits + ja-JP: キースペースのヒット数 + - field: keyspace_misses + type: 0 + i18n: + zh-CN: key命中失败次数 + en-US: Keyspace Misses + ja-JP: キースペースのミス数 + - field: pubsub_channels + type: 0 + i18n: + zh-CN: 订阅的频道数量 + en-US: Pubsub Channels + ja-JP: 有効になっているPub/Subチャネルに接続されているチャネルの数 + - field: pubsub_patterns + type: 0 + i18n: + zh-CN: 订阅的模式数量 + en-US: Pubsub Patterns + ja-JP: 有効になっているPub/Subパターンで接続されたパターンチャネルの数 + - field: latest_fork_usec + type: 0 + i18n: + zh-CN: 最近一次fork操作消耗时间(微秒) + en-US: Latest Fork Usec + ja-JP: 最後のフォーク作業にかかった時間 + - field: total_forks + type: 0 + i18n: + zh-CN: fork进程总数 + en-US: Total Forks + ja-JP: 実行されたフォーク作業の合計数 + - field: migrate_cached_sockets + type: 0 + i18n: + zh-CN: 正在进行migrate的Socket个数 + en-US: Migrate Cached Sockets + ja-JP: マイグレーションされたソケット数 + - field: slave_expires_tracked_keys + type: 0 + i18n: + zh-CN: 主从同步中已过期的key数量 + en-US: Slave Expires Tracked Keys + ja-JP: スレーブで満了した、トラッキングされたキーの数 + - field: active_defrag_hits + type: 0 + i18n: + zh-CN: 主动碎片整理命中次数 + en-US: Active Defrag Hits + ja-JP: アクティブなデフラグ操作中に発生したヒットの数 + - field: active_defrag_misses + type: 0 + i18n: + zh-CN: 主动碎片整理未命中次数 + en-US: Active Defrag Misses + ja-JP: アクティブなデフラグ操作中に見逃したヒットの数 + - field: active_defrag_key_hits + type: 0 + i18n: + zh-CN: 主动碎片整理key命中次数 + en-US: Active Defrag Key Hits + ja-JP: デフラグ操作でヒットしたキーの数 + - field: active_defrag_key_misses + type: 0 + i18n: + zh-CN: 主动碎片整理key未命中次数 + en-US: Active Defrag Key Misses + ja-JP: デフラグ操作で見逃したキーの数 + - field: tracking_total_keys + type: 0 + i18n: + zh-CN: 正在追踪的key数量 + en-US: Tracking Total Keys + ja-JP: トラッキングされたキーの合計数 + - field: tracking_total_items + type: 0 + i18n: + zh-CN: 正在追踪的value数量 + en-US: Tracking Total Items + ja-JP: トラッキングされたアイテムの合計数 + - field: tracking_total_prefixes + type: 0 + i18n: + zh-CN: 正在追踪的前缀数量 + en-US: Tracking Total Prefixes + ja-JP: トラッキングされたプレフィックスの合計数 + - field: unexpected_error_replies + type: 0 + i18n: + zh-CN: 执行命令时发生错误的数量 + en-US: Unexpected Error Replies + ja-JP: 予期しないエラー応答の数 + - field: total_error_replies + type: 0 + i18n: + zh-CN: 执行命令时发生错误的总数 + en-US: Total Error Replies + ja-JP: 発生したエラーの合計応答の数 + - field: dump_payload_sanitizations + type: 0 + i18n: + zh-CN: Dump命令时数据清理的数量 + en-US: Dump Payload Sanitizations + ja-JP: ダンプペイロードで実行された整理作業の数 + - field: total_reads_processed + type: 0 + i18n: + zh-CN: 执行读操作时处理的请求数量 + en-US: Total Reads Processed + ja-JP: 処理された読み取り作業の合計数 + - field: total_writes_processed + type: 0 + i18n: + zh-CN: 执行写操作时处理的请求数量 + en-US: Total Writes Processed + ja-JP: 処理された書き込み作業の合計数 + - field: io_threaded_reads_processed + type: 0 + i18n: + zh-CN: 使用线程进行读操作时处理的请求数量 + en-US: IO Threaded Reads Processed + ja-JP: I/Oスレッドで処理された読み取り作業の数 + - field: io_threaded_writes_processed + type: 0 + i18n: + zh-CN: 使用线程进行写操作时处理的请求数量 + en-US: IO Threaded Writes Processed + ja-JP: I/Oスレッドで処理された書き込み作業の数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + # metrics - replication + - name: replication + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 5 + i18n: + zh-CN: 主从同步信息 + en-US: Replication + ja-JP: レプリケーション情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: role + type: 1 + i18n: + zh-CN: 节点角色 + en-US: Role + ja-JP: ノード役割 + - field: connected_slaves + type: 0 + i18n: + zh-CN: 已连接的从节点个数 + en-US: Connected Slaves + ja-JP: 接続されたスレーブの数 + - field: master_failover_state + type: 1 + i18n: + zh-CN: 主从故障转移状态 + en-US: Master FailOver State + ja-JP: マスターノードのフェイルオーバー処理の状態 + - field: master_replid + type: 1 + i18n: + zh-CN: 主从同步id + en-US: Master Replid + ja-JP: マスターノードのレプリケーションID + - field: master_replid2 + type: 0 + i18n: + zh-CN: 主从同步id2 + en-US: Master Replid2 + ja-JP: マスターノードのレプリケーションID2 + - field: master_repl_offset + type: 0 + i18n: + zh-CN: 主节点偏移量 + en-US: Master Repl Offset + ja-JP: マスターノードの複製におけるオフセット値 + - field: second_repl_offset + type: 0 + i18n: + zh-CN: 接受主从同步的从节点偏移量 + en-US: Second Repl Offset + ja-JP: マスタとセカンダリ間の複製におけるオフセット値 + - field: repl_backlog_active + type: 0 + i18n: + zh-CN: 复制缓冲区状态 + en-US: Repl Backlog Active + ja-JP: 複製バックログの状態 + - field: repl_backlog_size + type: 0 + i18n: + zh-CN: 复制缓冲区大小(字节) + en-US: Repl Backlog Size + ja-JP: 複製バックログのバイト数 + - field: repl_backlog_first_byte_offset + type: 0 + i18n: + zh-CN: 复制缓冲区起始偏移量 + en-US: Repl Backlog First Byte Offset + ja-JP: 複製バックログにおける最初のバイトのオフセット + - field: repl_backlog_histlen + type: 0 + i18n: + zh-CN: 复制缓冲区的有效数据长度 + en-US: Repl Backlog Histlen + ja-JP: 複製バックログの履歴の長さ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + # metrics - cpu + - name: cpu + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 6 + i18n: + zh-CN: CPU消耗信息 + en-US: CPU + ja-JP: CPU情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: used_cpu_sys + type: 0 + i18n: + zh-CN: Redis进程使用的CPU时钟总和(内核态) + en-US: Used CPU Sys + ja-JP: システムモードで使用されたCPUの合計時間 + - field: used_cpu_user + type: 0 + i18n: + zh-CN: Redis进程使用的CPU时钟总和(用户态) + en-US: Used CPU User + ja-JP: ユーザーモードで使用されたCPUの合計時間 + - field: used_cpu_sys_children + type: 0 + i18n: + zh-CN: 后台进程使用的CPU时钟总和(内核态) + en-US: Used CPU Sys Children + ja-JP: 子プロセスがシステムモードで使用したCPU時間 + - field: used_cpu_user_children + type: 0 + i18n: + zh-CN: 后台进程使用的CPU时钟总和(用户态) + en-US: Used CPU User Children + ja-JP: 子プロセスがユーザーモードで使用したCPU時間 + - field: used_cpu_sys_main_thread + type: 0 + i18n: + zh-CN: 主进程使用的CPU时钟总和(内核态) + en-US: Used CPU Sys Main Thread + ja-JP: プロセスがシステムモードで使用したCPU時間 + - field: used_cpu_user_main_thread + type: 0 + i18n: + zh-CN: 主进程使用的CPU时钟总和(用户态) + en-US: Used CPU User Main Thread + ja-JP: プロセスがユーザーモードで使用したCPU時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + # metrics - errorstats + - name: errorstats + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 8 + i18n: + zh-CN: 错误信息 + en-US: Error Stats + ja-JP: エラー統計情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: errorstat_ERR + type: 1 + i18n: + zh-CN: 执行命令时出错的次数 + en-US: Error Stat Error + ja-JP: コマンド実行期間のエラー発生数 + - field: errorstat_MISCONF + type: 1 + i18n: + zh-CN: 执行命令时出现misconf错误的次数 + en-US: Error Stat Misconf + ja-JP: コマンド実行期間のmisconfエラー発生数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + # metrics - cluster + - name: cluster + # collect metrics content + priority: 9 + i18n: + zh-CN: 集群信息 + en-US: Cluster + ja-JP: クラスター情報 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: cluster_enabled + type: 0 + i18n: + zh-CN: 节点是否开启集群模式 + en-US: Cluster Enabled + ja-JP: クラスターの有効化 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + # metrics - commandstats + - name: commandstats + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 9 + i18n: + zh-CN: 命令统计信息 + en-US: Command Stats + ja-JP: コマンド統計情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: cmdstat_set + type: 1 + i18n: + zh-CN: SET 命令统计 + en-US: SET COMMAND STAT + ja-JP: SET コマンド統計 + - field: cmdstat_get + type: 1 + i18n: + zh-CN: GET 命令统计 + en-US: GET COMMAND STAT + ja-JP: GET コマンド統計 + - field: cmdstat_setnx + type: 1 + i18n: + zh-CN: SETNX 命令统计 + en-US: SETNX COMMAND STAT + ja-JP: SETNX コマンド統計 + - field: cmdstat_hset + type: 1 + i18n: + zh-CN: HSET 命令统计 + en-US: HSET COMMAND STAT + ja-JP: HSET コマンド統計 + - field: cmdstat_hget + type: 1 + i18n: + zh-CN: HGET 命令统计 + en-US: HGET COMMAND STAT + ja-JP: HGET コマンド統計 + - field: cmdstat_lpush + type: 1 + i18n: + zh-CN: LPUSH 命令统计 + en-US: LPUSH COMMAND STAT + ja-JP: LPUSH コマンド統計 + - field: cmdstat_rpush + type: 1 + i18n: + zh-CN: RPUSH 命令统计 + en-US: RPUSH COMMAND STAT + ja-JP: RPUSH コマンド統計 + - field: cmdstat_lpop + type: 1 + i18n: + zh-CN: LPOP 命令统计 + en-US: LPOP COMMAND STAT + ja-JP: LPOP コマンド統計 + - field: cmdstat_rpop + type: 1 + i18n: + zh-CN: RPOP 命令统计 + en-US: RPOP COMMAND STAT + ja-JP: RPOP コマンド統計 + - field: cmdstat_llen + type: 1 + i18n: + zh-CN: LLEN 命令统计 + en-US: LLEN COMMAND STAT + ja-JP: LLEN コマンド統計 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + # metrics - keyspace + - name: keyspace + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 9 + i18n: + zh-CN: 键空间 + en-US: Keyspace + ja-JP: キー空間 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: db0 + type: 1 + i18n: + zh-CN: db0 + en-US: db0 + ja-JP: db0 + - field: db1 + type: 1 + i18n: + zh-CN: db1 + en-US: db1 + ja-JP: db1 + - field: db2 + type: 1 + i18n: + zh-CN: db2 + en-US: db2 + ja-JP: db2 + - field: db3 + type: 1 + i18n: + zh-CN: db3 + en-US: db3 + ja-JP: db3 + - field: db4 + type: 1 + i18n: + zh-CN: db4 + en-US: db4 + ja-JP: db4 + - field: db5 + type: 1 + i18n: + zh-CN: db5 + en-US: db5 + ja-JP: db5 + - field: db6 + type: 1 + i18n: + zh-CN: db6 + en-US: db6 + ja-JP: db6 + - field: db7 + type: 1 + i18n: + zh-CN: db7 + en-US: db7 + ja-JP: db7 + - field: db8 + type: 1 + i18n: + zh-CN: db8 + en-US: db8 + ja-JP: db8 + - field: db9 + type: 1 + i18n: + zh-CN: db9 + en-US: db9 + ja-JP: db9 + - field: db10 + type: 1 + i18n: + zh-CN: db10 + en-US: db10 + ja-JP: db10 + - field: db11 + type: 1 + i18n: + zh-CN: db11 + en-US: db11 + ja-JP: db11 + - field: db12 + type: 1 + i18n: + zh-CN: db12 + en-US: db12 + ja-JP: db12 + - field: db13 + type: 1 + i18n: + zh-CN: db13 + en-US: db13 + ja-JP: db13 + - field: db14 + type: 1 + i18n: + zh-CN: db14 + en-US: db14 + ja-JP: db14 + - field: db15 + type: 1 + i18n: + zh-CN: db15 + en-US: db15 + ja-JP: db15 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-redis_cluster.yml b/hertzbeat-manager/src/main/resources/define/app-redis_cluster.yml new file mode 100644 index 0000000..b41f2c4 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-redis_cluster.yml @@ -0,0 +1,1775 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: cache +# The monitoring type eg: linux windows tomcat mysql aws... +app: redis_cluster +# The monitoring i18n name +name: + zh-CN: Redis Cluster + en-US: Redis Cluster + ja-JP: Redis クラスター +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Redis 集群 的通用指标进行采集监控。<br>您可以点击 “<i>新建 Redis Cluster</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors Redis Database Cluster's general performance metrics. You could click the "<i>New Redis Cluster</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 Redis 集群 的通用指標進行采集監控。<br>您可以點擊 “<i>新建 Redis Cluster</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は Redis クラスターの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Redis クラスター</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/redis_cluster + en-US: https://hertzbeat.apache.org/docs/help/redis_cluster +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 6379 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # default value + defaultValue: 3000 + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: pattern + # name-param field display i18n name + name: + zh-CN: 模式 + en-US: Pattern + ja-JP: パターン + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: true + # default value + defaultValue: 3 + # hide-is hide this field and put it in advanced layout + hide: true + - field: enableSshTunnel + name: + zh-CN: 是否启用SSH隧道 + en-US: Enable SSH Tunnel + ja-JP: SSHトンネルの有効化 + type: boolean + required: true + hide: true + - field: sshHost + name: + zh-CN: SSH Host + en-US: SSH Host + ja-JP: SSHホスト + type: text + required: false + placeholder: 'When Enable SSH Tunnel' + hide: true + - field: sshPort + name: + zh-CN: SSH端口 + en-US: SSH Port + ja-JP: SSHポート + type: number + range: '[0,65535]' + required: false + defaultValue: 22 + placeholder: 'When Enable SSH tunnel' + hide: true + - field: sshTimeout + name: + zh-CN: SSH超时时间(ms) + en-US: SSH Timeout(ms) + ja-JP: SSHタイムアウト(ms) + type: number + required: false + range: '[400,200000]' + defaultValue: 6000 + hide: true + - field: sshUsername + name: + zh-CN: SSH用户名 + en-US: SSH Username + ja-JP: SSHユーザー名 + type: text + required: false + placeholder: 'When Enable SSH tunnel' + hide: true + - field: sshPassword + name: + zh-CN: SSH密码 + en-US: SSH Password + ja-JP: SSHパスワード + type: password + required: false + hide: true + - field: sshShareConnection + name: + zh-CN: 是否共享SSH连接 + en-US: Share SSH Connection + ja-JP: SSH接続共有 + type: boolean + required: true + defaultValue: true + hide: true + - field: sshPrivateKey + name: + zh-CN: SSH私钥 + en-US: SSH PrivateKey + ja-JP: SSH秘密鍵 + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + required: false + hide: true + - field: sshPrivateKeyPassphrase + name: + zh-CN: SSH密钥短语 + en-US: SSH PrivateKey PassPhrase + ja-JP: SSH秘密鍵フレーズ + type: password + required: false + hide: true +metrics: + - name: server + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + i18n: + zh-CN: 服务器信息 + en-US: Server + ja-JP: サーバー情報 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: 标识 + en-US: Identity + ja-JP: ID + - field: redis_version + type: 1 + i18n: + zh-CN: Redis 版本 + en-US: Redis Version + ja-JP: Redisバージョン + - field: redis_git_sha1 + type: 0 + i18n: + zh-CN: Redis Git SHA1 + en-US: Redis Git SHA1 + ja-JP: Redis Git SHA1 + - field: redis_git_dirty + type: 0 + i18n: + zh-CN: Redis Git Dirty + en-US: Redis Git Dirty + ja-JP: RedisサーバーのGitリポジトリ状態 + - field: redis_build_id + type: 1 + i18n: + zh-CN: Redis 构建 ID + en-US: Redis Build ID + ja-JP: Redis ビルド Id + - field: redis_mode + type: 1 + i18n: + zh-CN: Redis 模式 + en-US: Redis Mode + ja-JP: Redis モード + - field: os + type: 1 + i18n: + zh-CN: 操作系统 + en-US: Operating System + ja-JP: オーエス + - field: arch_bits + type: 0 + i18n: + zh-CN: 架构位数 + en-US: Architecture Bits + ja-JP: アーキテクチャ + - field: multiplexing_api + type: 1 + i18n: + zh-CN: 多路复用 API + en-US: Multiplexing API + ja-JP: IO多重化API + - field: atomicvar_api + type: 1 + i18n: + zh-CN: 原子变量 API + en-US: Atomicvar API + ja-JP: 原子操作API + - field: gcc_version + type: 1 + i18n: + zh-CN: GCC 版本 + en-US: GCC Version + ja-JP: GCC バージョン + - field: process_id + type: 0 + i18n: + zh-CN: 进程 ID + en-US: Process ID + ja-JP: プロセスID + - field: process_supervised + type: 1 + i18n: + zh-CN: 进程监控 + en-US: Process Supervised + ja-JP: プロセスの監視方法 + - field: run_id + type: 1 + i18n: + zh-CN: 运行 ID + en-US: Run ID + ja-JP: Run ID + - field: tcp_port + type: 0 + i18n: + zh-CN: TCP 端口 + en-US: TCP Port + ja-JP: TCP ポート + - field: server_time_usec + type: 0 + i18n: + zh-CN: 基于纪元的系统时间 + en-US: Server Time Usec + ja-JP: サーバーのタイムスタンプ + - field: uptime_in_seconds + type: 0 + i18n: + zh-CN: 启动时间(秒) + en-US: Uptime In Seconds + ja-JP: アップタイム(秒) + - field: uptime_in_days + type: 0 + i18n: + zh-CN: 启动时间(天) + en-US: Uptime In Days + ja-JP: アップタイム(日) + - field: hz + type: 0 + i18n: + zh-CN: 定时器频率 + en-US: HZ + ja-JP: イベントの実行頻度 + - field: configured_hz + type: 0 + i18n: + zh-CN: 配置的定时器频率 + en-US: Configured HZ + ja-JP: 設定されたイベントの実行頻度 + - field: lru_clock + type: 0 + i18n: + zh-CN: LRU 时钟 + en-US: LRU Clock + ja-JP: LRU クロック + - field: executable + type: 1 + i18n: + zh-CN: 可执行文件 + en-US: Executable + ja-JP: サーバーの実行パス + - field: config_file + type: 1 + i18n: + zh-CN: 配置文件 + en-US: Config File + ja-JP: 設定されたサーバーの実行ファイル + - field: io_threads_active + type: 0 + i18n: + zh-CN: IO 线程活跃数 + en-US: IO Threads Active + ja-JP: 活動中のスレッド数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + + - name: clients + i18n: + zh-CN: 客户端 + en-US: Clients + ja-JP: クライアント情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: 标识 + en-US: Identity + ja-JP: ID + - field: connected_clients + type: 0 + i18n: + zh-CN: 已连接客户端 + en-US: Connected Clients + ja-JP: 現在接続されているクライアントの数 + - field: cluster_connections + type: 0 + i18n: + zh-CN: 集群连接数 + en-US: Cluster Connections + ja-JP: クラスター内の接続数 + - field: maxclients + type: 0 + i18n: + zh-CN: 最大客户端数 + en-US: Maxclients + ja-JP: 最大クライアント数 + - field: client_recent_max_input_buffer + type: 0 + i18n: + zh-CN: 客户端最近最大输入缓冲区 + en-US: Client Recent Max Input Buffer + ja-JP: クライアントの最近の最大入力バッファサイズ + - field: client_recent_max_output_buffer + type: 0 + i18n: + zh-CN: 客户端最近最大输出缓冲区 + en-US: Client Recent Max Output Buffer + ja-JP: クライアントの最近の最大出力バッファサイズ + - field: blocked_clients + type: 0 + i18n: + zh-CN: 阻塞客户端 + en-US: Blocked Clients + ja-JP: ブロックされたクライアント数 + - field: tracking_clients + type: 0 + i18n: + zh-CN: 跟踪客户端 + en-US: Tracking Clients + ja-JP: トラッキングを使用するクライアントの数 + - field: clients_in_timeout_table + type: 0 + i18n: + zh-CN: 超时表中的客户端 + en-US: Clients In Timeout Table + ja-JP: タイムアウトテーブル内のクライアント数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: memory + i18n: + zh-CN: 内存 + en-US: Memory + ja-JP: メモリ情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: 标识 + en-US: Identity + ja-JP: ID + - field: used_memory + type: 0 + i18n: + zh-CN: 已使用内存 + en-US: Used Memory + ja-JP: 使用したメモリ(バイト) + - field: used_memory_human + type: 0 + i18n: + zh-CN: 已使用内存 (人类可读) + en-US: Used Memory Human + ja-JP: 使用したメモリ + unit: MB + - field: used_memory_rss + type: 0 + i18n: + zh-CN: 已使用内存 RSS + en-US: Used Memory RSS + ja-JP: 使用した物理メモリ(バイト) + - field: used_memory_rss_human + type: 0 + i18n: + zh-CN: 已使用内存 RSS (人类可读) + en-US: Used Memory RSS Human + ja-JP: 使用した物理メモリ + unit: MB + - field: used_memory_peak + type: 0 + i18n: + zh-CN: 内存使用峰值 + en-US: Used Memory Peak + ja-JP: 使用したメモリの最大値(バイト) + - field: used_memory_peak_human + type: 0 + i18n: + zh-CN: 内存使用峰值 (人类可读) + en-US: Used Memory Peak Human + ja-JP: 使用したメモリの最大値 + unit: MB + - field: used_memory_peak_perc + type: 0 + i18n: + zh-CN: 内存使用峰值百分比 + en-US: Used Memory Peak Perc + ja-JP: メモリの最大使用率 + unit: '%' + - field: used_memory_overhead + type: 0 + i18n: + zh-CN: 内存开销 + en-US: Used Memory Overhead + ja-JP: オーバーヘッドのメモリ使用量 + - field: used_memory_startup + type: 0 + i18n: + zh-CN: 启动时内存使用 + en-US: Used Memory Startup + ja-JP: 起動時に使用したメモリ量 + - field: used_memory_dataset + type: 0 + i18n: + zh-CN: 数据集内存使用 + en-US: Used Memory Dataset + ja-JP: データセットが使用中のメモリ量 + - field: used_memory_dataset_perc + type: 0 + i18n: + zh-CN: 数据集内存使用百分比 + en-US: Used Memory Dataset Perc + ja-JP: データセットが使用中のメモリ率 + unit: '%' + - field: allocator_allocated + type: 0 + i18n: + zh-CN: 分配器已分配内存 + en-US: Allocator Allocated + ja-JP: 実際に割り当てられたメモリ量 + - field: allocator_active + type: 0 + i18n: + zh-CN: 分配器活跃内存 + en-US: Allocator Active + ja-JP: メモリ割り当て器がオーエスに要求したメモリ量 + - field: allocator_resident + type: 0 + i18n: + zh-CN: 分配器驻留内存 + en-US: Allocator Resident + ja-JP: メモリ割り当て器が実際に使用中のメモリ量 + - field: total_system_memory + type: 0 + i18n: + zh-CN: 系统总内存 + en-US: Total System Memory + ja-JP: 使用可能なシステムメモリの合計(バイト) + - field: total_system_memory_human + type: 0 + i18n: + zh-CN: 系统总内存 (人类可读) + en-US: Total System Memory Human + ja-JP: 使用可能なシステムメモリの合計 + unit: GB + - field: used_memory_lua + type: 0 + i18n: + zh-CN: Lua 脚本内存使用 + en-US: Used Memory Lua + ja-JP: Luaスクリプトが使用中のメモリの量(バイト) + - field: used_memory_lua_human + type: 0 + i18n: + zh-CN: Lua 脚本内存使用 (人类可读) + en-US: Used Memory Lua Human + ja-JP: Luaスクリプトが使用しているメモリの量 + unit: KB + - field: used_memory_scripts + type: 0 + i18n: + zh-CN: 脚本内存使用 + en-US: Used Memory Scripts + ja-JP: キャッシュされたLuaスクリプトのメモリ量(バイト) + - field: used_memory_scripts_human + type: 0 + i18n: + zh-CN: 脚本内存使用 (人类可读) + en-US: Used Memory Scripts Human + ja-JP: キャッシュされているLuaスクリプトのメモリ量 + unit: KB + - field: number_of_cached_scripts + type: 0 + i18n: + zh-CN: 缓存脚本数 + en-US: Number Of Cached Scripts + ja-JP: キャッシュされたLuaスクリプトの数 + - field: maxmemory + type: 0 + i18n: + zh-CN: 最大内存 + en-US: Maxmemory + ja-JP: メモリの最大容量(バイト) + - field: maxmemory_human + type: 0 + i18n: + zh-CN: 最大内存 (人类可读) + en-US: Maxmemory Human + ja-JP: メモリの最大容量 + unit: MB + - field: maxmemory_policy + type: 1 + i18n: + zh-CN: 最大内存策略 + en-US: Maxmemory Policy + ja-JP: キーの削除ポリシー + - field: allocator_frag_ratio + type: 0 + i18n: + zh-CN: 分配器碎片率 + en-US: Allocator Frag Ratio + ja-JP: メモリ割り当て器のメモリ断片化の比率 + - field: allocator_frag_bytes + type: 0 + i18n: + zh-CN: 分配器碎片字节 + en-US: Allocator Frag Bytes + ja-JP: メモリ割り当て器のメモリ断片化のバイト数 + - field: allocator_rss_ratio + type: 0 + i18n: + zh-CN: 分配器驻留内存比率 + en-US: Allocator Rss Ratio + ja-JP: メモリ割り当て器の物理メモリの比率 + - field: allocator_rss_bytes + type: 0 + i18n: + zh-CN: 分配器驻留内存字节 + en-US: Allocator Rss Bytes + ja-JP: メモリ割り当て器の実際に使用中の物理メモリのバイト数 + - field: rss_overhead_ratio + type: 0 + i18n: + zh-CN: RSS 开销比率 + en-US: Rss Overhead Ratio + ja-JP: 物理メモリオーバーヘッドの比率 + - field: rss_overhead_bytes + type: 0 + i18n: + zh-CN: RSS 开销字节 + en-US: Rss Overhead Bytes + ja-JP: 物理メモリオーバーヘッドのバイト数 + - field: mem_fragmentation_ratio + type: 0 + i18n: + zh-CN: 内存碎片率 + en-US: Mem Fragmentation Ratio + ja-JP: メモリ断片化の比率 + - field: mem_fragmentation_bytes + type: 0 + i18n: + zh-CN: 内存碎片字节 + en-US: Mem Fragmentation Bytes + ja-JP: メモリ断片化によって発生したバイト数 + - field: mem_not_counted_for_evict + type: 0 + i18n: + zh-CN: 不计入驱逐的内存 + en-US: Mem Not Counted For Evict + ja-JP: 強制抽出の対象ではないメモリの量 + - field: mem_replication_backlog + type: 0 + i18n: + zh-CN: 复制积压内存 + en-US: Mem Replication Backlog + ja-JP: 複製バックログが使用中のメモリの量 + - field: mem_clients_slaves + type: 0 + i18n: + zh-CN: 从节点客户端数 + en-US: Mem Clients Slaves + ja-JP: スレーブクライアントが使用中のメモリの量 + - field: mem_clients_normal + type: 0 + i18n: + zh-CN: 普通客户端数 + en-US: Mem Clients Normal + ja-JP: クライアントが使用中のメモリの量 + - field: mem_aof_buffer + type: 0 + i18n: + zh-CN: AOF 缓冲区内存 + en-US: Mem Aof Buffer + ja-JP: AOFバッファで使用中のメモリ量 + - field: mem_allocator + type: 1 + i18n: + zh-CN: 内存分配器 + en-US: Mem Allocator + ja-JP: メモリ割り当て器 + - field: active_defrag_running + type: 0 + i18n: + zh-CN: 活跃碎片整理运行中 + en-US: Active Defrag Running + ja-JP: アクティブなデフラグ操作のステータス + - field: lazyfree_pending_objects + type: 0 + i18n: + zh-CN: 懒惰释放待处理对象 + en-US: Lazyfree Pending Objects + ja-JP: Lazyfree作業が保留中のオブジェクトの数 + - field: lazyfreed_objects + type: 0 + i18n: + zh-CN: 懒惰释放对象 + en-US: Lazyfreed Objects + ja-JP: Lazyfre作業で解除されたオブジェクトの数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: persistence + i18n: + zh-CN: 持久化 + en-US: Persistence + ja-JP: 永続化情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: 标识 + en-US: Identity + ja-JP: ID + - field: loading + type: 0 + i18n: + zh-CN: 加载中 + en-US: Loading + ja-JP: データロード有無 + - field: current_cow_size + type: 0 + i18n: + zh-CN: 当前 COW 大小 + en-US: Current Cow Size + ja-JP: 現在COWのサイズ + - field: current_cow_size_age + type: 0 + i18n: + zh-CN: 当前 COW 大小年龄 + en-US: Current Cow Size Age + ja-JP: 現在COWのサイズスパン + - field: current_fork_perc + type: 0 + i18n: + zh-CN: 当前 Fork 百分比 + en-US: Current Fork Perc + ja-JP: 現在のフォーク作業の進行率 + - field: current_save_keys_processed + type: 0 + i18n: + zh-CN: 当前保存键处理 + en-US: Current Save Keys Processed + ja-JP: 現在の保存作業で処理されたキーの数 + - field: current_save_keys_total + type: 0 + i18n: + zh-CN: 当前保存键总数 + en-US: Current Save Keys Total + ja-JP: 現在の保存作業で処理するキー数の合計 + - field: rdb_changes_since_last_save + type: 0 + i18n: + zh-CN: 上次保存后的更改 + en-US: Rdb Changes Since Last Save + ja-JP: RDBファイルを最後に保存して以来、入力されたデータ件数 + - field: rdb_bgsave_in_progress + type: 0 + i18n: + zh-CN: RDB 后台保存中 + en-US: Rdb Bgsave In Progress + ja-JP: バックグラウンドRDBの保存作業が進行中 + - field: rdb_last_save_time + type: 0 + i18n: + zh-CN: RDB 上次保存时间 + en-US: Rdb Last Save Time + ja-JP: 最後のRDB保存時刻 + - field: rdb_last_bgsave_status + type: 1 + i18n: + zh-CN: RDB 上次后台保存状态 + en-US: Rdb Last Bgsave Status + ja-JP: 最後のRDB保存ステータス + - field: rdb_last_bgsave_time_sec + type: 0 + i18n: + zh-CN: RDB 上次后台保存时间 + en-US: Rdb Last Bgsave Time + ja-JP: 最後のバックグラウンドRDB保存にかかった時間(秒) + - field: rdb_current_bgsave_time_sec + type: 0 + i18n: + zh-CN: RDB 当前后台保存时间 + en-US: Rdb Current Bgsave Time + ja-JP: 現在バックグラウンドでRDBを保存している場合、保存開始から現在までの所要時間(秒) + - field: rdb_last_cow_size + type: 0 + i18n: + zh-CN: RDB 上次 COW 大小 + en-US: Rdb Last Cow Size + ja-JP: 最後のRDB保存時点のCOWのサイズ + - field: aof_enabled + type: 0 + i18n: + zh-CN: AOF 启用 + en-US: Aof Enabled + ja-JP: AOFの活性化状態の有無 + - field: aof_rewrite_in_progress + type: 0 + i18n: + zh-CN: AOF 重写中 + en-US: Aof Rewrite In Progress + ja-JP: バックグラウンドAOFの保存作業が進行中 + - field: aof_rewrite_scheduled + type: 0 + i18n: + zh-CN: AOF 重写周期 + en-US: Aof Rewrite Scheduled + ja-JP: AOF rewriteを実行する予定 + - field: aof_last_rewrite_time_sec + type: 0 + i18n: + zh-CN: AOF 上次重写时间 + en-US: Aof Last Rewrite Time + ja-JP: 最後のAOF rewriteにかかった時間(秒) + - field: aof_current_rewrite_time_sec + type: 0 + i18n: + zh-CN: AOF 当前重写时间 + en-US: Aof Current Rewrite Time + ja-JP: 現在AOF rewriteにかかった時間(秒) + - field: aof_last_bgrewrite_status + type: 1 + i18n: + zh-CN: AOF 上次后台重写状态 + en-US: Aof Last Bgrewrite Status + ja-JP: 最後のAOF rewriteのステータス + - field: aof_last_write_status + type: 1 + i18n: + zh-CN: AOF 上次写入状态 + en-US: Aof Last Write Status + ja-JP: 最後のAOF書き込みのステータス + - field: aof_last_cow_size + type: 0 + i18n: + zh-CN: AOF 上次 COW 大小 + en-US: Aof Last Cow Size + ja-JP: 最後のAOF rewrite時点のCOWのサイズ + - field: module_fork_in_progress + type: 0 + i18n: + zh-CN: 模块 Fork 中 + en-US: Module Fork In Progress + ja-JP: モジュールフォークの進行有無 + - field: module_fork_last_cow_size + type: 0 + i18n: + zh-CN: 模块 Fork 上次 COW 大小 + en-US: Module Fork Last Cow Size + ja-JP: 最後のモジュールフォーク時点のCOWのサイズ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: stats + i18n: + zh-CN: 统计 + en-US: Stats + ja-JP: 統計情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 4 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: 标识 + en-US: Identity + ja-JP: ID + - field: total_connections_received + type: 0 + i18n: + zh-CN: 收到总连接数 + en-US: Total Connections Received + ja-JP: サーバーの起動後に受信された合計接続数 + - field: total_commands_processed + type: 0 + i18n: + zh-CN: 处理总命令数 + en-US: Total Commands Processed + ja-JP: サーバーの起動後に処理した総コマンド数 + - field: instantaneous_ops_per_sec + type: 0 + i18n: + zh-CN: 每秒操作数 + en-US: Instantaneous Ops Per Sec + ja-JP: 1秒当たりに処理されたコマンドの数 + - field: total_net_input_bytes + type: 0 + i18n: + zh-CN: 网络总输入字节数 + en-US: Total Net Input Bytes + ja-JP: 受信されたネットワークバイトの合計数 + - field: total_net_output_bytes + type: 0 + i18n: + zh-CN: 网络总输出字节数 + en-US: Total Net Output Bytes + ja-JP: 転送されたネットワークバイトの合計数 + - field: instantaneous_input_kbps + type: 0 + i18n: + zh-CN: 瞬时的每秒输入 KB + en-US: Instantaneous Input KB + ja-JP: 瞬間入力速度(kbps) + - field: instantaneous_output_kbps + type: 0 + i18n: + zh-CN: 瞬时的每秒输出 KB + en-US: Instantaneous Output KB + ja-JP: 瞬間出力速度(kbps) + - field: rejected_connections + type: 0 + i18n: + zh-CN: 拒绝连接数 + en-US: Rejected Connections + ja-JP: maxclients制限で拒否された接続数 + - field: sync_full + type: 0 + i18n: + zh-CN: 同步全量 + en-US: Sync Full + ja-JP: 完全同期の回数 + - field: sync_partial_ok + type: 0 + i18n: + zh-CN: 同步部分成功 + en-US: Sync Partial Ok + ja-JP: 部分同期の回数 + - field: sync_partial_err + type: 0 + i18n: + zh-CN: 同步部分失败 + en-US: Sync Partial Err + ja-JP: 部分同期のエラー回数 + - field: expired_keys + type: 0 + i18n: + zh-CN: 过期键 + en-US: Expired Keys + ja-JP: expireコマンドで削除されたキーの数 + - field: expired_stale_perc + type: 0 + i18n: + zh-CN: 过期陈旧百分比 + en-US: Expired Stale Perc + ja-JP: 期限切れのステイルキーの比率 + - field: expired_time_cap_reached_count + type: 0 + i18n: + zh-CN: 达到过期时间上限计数 + en-US: Expired Time Cap Reached Count + ja-JP: 満了時間の制限に達した回数 + - field: expire_cycle_cpu_milliseconds + type: 0 + i18n: + zh-CN: 过期周期 CPU 毫秒 + en-US: Expire Cycle CPU Milliseconds + ja-JP: 満了サイクルでかかったCPU時間(ms) + - field: evicted_keys + type: 0 + i18n: + zh-CN: 驱逐键 + en-US: Evicted Keys + ja-JP: メモリ不足で追放されたキーの数 + - field: keyspace_hits + type: 0 + i18n: + zh-CN: 命中键空间 + en-US: Keyspace Hits + ja-JP: キースペースのヒット数 + - field: keyspace_misses + type: 0 + i18n: + zh-CN: 未命中键空间 + en-US: Keyspace Misses + ja-JP: キースペースのミス数 + - field: pubsub_channels + type: 0 + i18n: + zh-CN: 发布订阅频道 + en-US: Pubsub Channels + ja-JP: 有効になっているPub/Subチャネルに接続されているチャネルの数 + - field: pubsub_patterns + type: 0 + i18n: + zh-CN: 发布订阅模式 + en-US: Pubsub Patterns + ja-JP: 有効になっているPub/Subパターンで接続されたパターンチャネルの数 + - field: latest_fork_usec + type: 0 + i18n: + zh-CN: 最新 Fork 微秒 + en-US: Latest Fork Usec + ja-JP: 最後のフォーク作業にかかった時間 + - field: total_forks + type: 0 + i18n: + zh-CN: 总 Fork 数 + en-US: Total Forks + ja-JP: 実行されたフォーク作業の合計数 + - field: migrate_cached_sockets + type: 0 + i18n: + zh-CN: 迁移缓存套接字 + en-US: Migrate Cached Sockets + ja-JP: マイグレーションされたソケット数 + - field: slave_expires_tracked_keys + type: 0 + i18n: + zh-CN: 从节点过期跟踪键 + en-US: Slave Expires Tracked Keys + ja-JP: スレーブで満了した、トラッキングされたキーの数 + - field: active_defrag_hits + type: 0 + i18n: + zh-CN: 活跃碎片整理命中 + en-US: Active Defrag Hits + ja-JP: アクティブなデフラグ操作中に発生したヒットの数 + - field: active_defrag_misses + type: 0 + i18n: + zh-CN: 活跃碎片整理未命中 + en-US: Active Defrag Misses + ja-JP: アクティブなデフラグ操作中に見逃したヒットの数 + - field: active_defrag_key_hits + type: 0 + i18n: + zh-CN: 活跃碎片整理键命中 + en-US: Active Defrag Key Hits + ja-JP: デフラグ操作でヒットしたキーの数 + - field: active_defrag_key_misses + type: 0 + i18n: + zh-CN: 活跃碎片整理键未命中 + en-US: Active Defrag Key Misses + ja-JP: デフラグ操作で見逃したキーの数 + - field: tracking_total_keys + type: 0 + i18n: + zh-CN: 跟踪键总数 + en-US: Tracking Total Keys + ja-JP: トラッキングされたキーの合計数 + - field: tracking_total_items + type: 0 + i18n: + zh-CN: 跟踪项总数 + en-US: Tracking Total Items + ja-JP: トラッキングされたアイテムの合計数 + - field: tracking_total_prefixes + type: 0 + i18n: + zh-CN: 跟踪前缀总数 + en-US: Tracking Total Prefixes + ja-JP: トラッキングされたプレフィックスの合計数 + - field: unexpected_error_replies + type: 0 + i18n: + zh-CN: 意外错误回复 + en-US: Unexpected Error Replies + ja-JP: 予期しないエラー応答の数 + - field: total_error_replies + type: 0 + i18n: + zh-CN: 总错误回复 + en-US: Total Error Replies + ja-JP: 発生したエラーの合計応答の数 + - field: dump_payload_sanitizations + type: 0 + i18n: + zh-CN: 转储有效负载深度完整性验证的总数 + en-US: Dump Payload Sanitizations + ja-JP: ダンプペイロードで実行された整理作業の数 + - field: total_reads_processed + type: 0 + i18n: + zh-CN: 总读取处理 + en-US: Total Reads Processed + ja-JP: 処理された読み取り作業の合計数 + - field: total_writes_processed + type: 0 + i18n: + zh-CN: 总写入处理 + en-US: Total Writes Processed + ja-JP: 処理された書き込み作業の合計数 + - field: io_threaded_reads_processed + type: 0 + i18n: + zh-CN: IO 线程读取处理 + en-US: Io Threaded Reads Processed + ja-JP: I/Oスレッドで処理された読み取り作業の数 + - field: io_threaded_writes_processed + type: 0 + i18n: + zh-CN: IO 线程写入处理 + en-US: Io Threaded Writes Processed + ja-JP: I/Oスレッドで処理された書き込み作業の数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: replication + i18n: + zh-CN: 副本 + en-US: Replication + ja-JP: レプリケーション情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 5 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: 标识 + en-US: Identity + ja-JP: ID + - field: role + type: 1 + i18n: + zh-CN: 角色 + en-US: Role + ja-JP: ノード役割 + - field: connected_slaves + type: 0 + i18n: + zh-CN: 已连接从节点 + en-US: Connected Slaves + ja-JP: 接続されたスレーブの数 + - field: master_failover_state + type: 1 + i18n: + zh-CN: 主节点故障转移状态 + en-US: Master Failover State + ja-JP: マスターノードのフェイルオーバー処理の状態 + - field: master_replid + type: 1 + i18n: + zh-CN: 主节点复制 ID + en-US: Master Replid + ja-JP: マスターノードのレプリケーションID + - field: master_replid2 + type: 0 + i18n: + zh-CN: 主节点复制 ID2 + en-US: Master Replid2 + ja-JP: マスターノードのレプリケーションID2 + - field: master_repl_offset + type: 0 + i18n: + zh-CN: 主节点复制偏移量 + en-US: Master Repl Offset + ja-JP: マスターノードの複製におけるオフセット値 + - field: second_repl_offset + type: 0 + i18n: + zh-CN: 第二复制偏移量 + en-US: Second Repl Offset + ja-JP: マスタとセカンダリ間の複製におけるオフセット値 + - field: repl_backlog_active + type: 0 + i18n: + zh-CN: 复制积压活跃 + en-US: Repl Backlog Active + ja-JP: 複製バックログの状態 + - field: repl_backlog_size + type: 0 + i18n: + zh-CN: 复制积压大小 + en-US: Repl Backlog Size + ja-JP: 複製バックログのバイト数 + - field: repl_backlog_first_byte_offset + type: 0 + i18n: + zh-CN: 复制积压第一个字节偏移量 + en-US: Repl Backlog First Byte Offset + ja-JP: 複製バックログにおける最初のバイトのオフセット + - field: repl_backlog_histlen + type: 0 + i18n: + zh-CN: 复制积压历史长度 + en-US: Repl Backlog Histlen + ja-JP: 複製バックログの履歴の長さ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: cpu + i18n: + zh-CN: CPU + en-US: CPU + ja-JP: CPU情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 6 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: 标识 + en-US: Identity + ja-JP: ID + - field: used_cpu_sys + type: 0 + i18n: + zh-CN: 系统已使用 CPU + en-US: Sys CPU Used + ja-JP: システムモードで使用されたCPUの合計時間 + - field: used_cpu_user + type: 0 + i18n: + zh-CN: 用户已使用 CPU + en-US: User CPU Used + ja-JP: ユーザーモードで使用されたCPUの合計時間 + - field: used_cpu_sys_children + type: 0 + i18n: + zh-CN: Sys 子进程已使用 CPU + en-US: Sys Children CPU Used + ja-JP: 子プロセスがシステムモードで使用したCPU時間 + - field: used_cpu_user_children + type: 0 + i18n: + zh-CN: 用户子进程已使用 CPU + en-US: User Children CPU Used + ja-JP: 子プロセスがユーザーモードで使用したCPU時間 + - field: used_cpu_sys_main_thread + type: 0 + i18n: + zh-CN: 系统主线程已使用 CPU + en-US: Sys Main Thread CPU Used + ja-JP: プロセスがシステムモードで使用したCPU時間 + - field: used_cpu_user_main_thread + type: 0 + i18n: + zh-CN: 用户主线程已使用 CPU + en-US: User Main Thread CPU Used + ja-JP: プロセスがユーザーモードで使用したCPU時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: errorstats + i18n: + zh-CN: 错误统计 + en-US: Error Stats + ja-JP: エラー統計情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 8 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: 标识 + en-US: Identity + ja-JP: ID + - field: errorstat_ERR + type: 1 + i18n: + zh-CN: 错误统计 ERR + en-US: Error Stats ERR + ja-JP: コマンド実行期間のエラー発生数 + - field: errorstat_MISCONF + type: 1 + i18n: + zh-CN: 错误统计 MISCONF + en-US: Error Stats MISCONF + ja-JP: コマンド実行期間のmisconfエラー発生数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: cluster + i18n: + zh-CN: 集群 + en-US: Cluster + ja-JP: クラスター情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 9 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: 标识 + en-US: Identity + ja-JP: ID + - field: cluster_enabled + type: 0 + i18n: + zh-CN: 集群启用 + en-US: Cluster Enabled + ja-JP: クラスターの有効化 + - field: cluster_state + type: 1 + i18n: + zh-CN: 集群状态 + en-US: Cluster State + ja-JP: クラスター状態 + - field: cluster_slots_assigned + type: 0 + i18n: + zh-CN: 集群分配槽 + en-US: Cluster Slots Assigned + ja-JP: 割り当てられたスロットの総数 + - field: cluster_slots_ok + type: 0 + i18n: + zh-CN: 集群槽正常 + en-US: Cluster Slots Ok + ja-JP: すべてのスロットの状態 + - field: cluster_slots_pfail + type: 0 + i18n: + zh-CN: 集群槽部分失败 + en-US: Cluster Slots Pfail + ja-JP: PFAIL状態のハッシュスロットの数 + - field: cluster_slots_fail + type: 0 + i18n: + zh-CN: 集群槽失败 + en-US: Cluster Slots Fail + ja-JP: FAIL状態のハッシュスロットの数 + - field: cluster_known_nodes + type: 0 + i18n: + zh-CN: 集群已知节点 + en-US: Cluster Known Nodes + ja-JP: クラスター内のノード数 + - field: cluster_size + type: 0 + i18n: + zh-CN: 集群大小 + en-US: Cluster Size + ja-JP: マスターノードの数 + - field: cluster_current_epoch + type: 0 + i18n: + zh-CN: 集群当前 epoch + en-US: Cluster Current Epoch + ja-JP: 現在のマスターのepoch値 + - field: cluster_my_epoch + type: 0 + i18n: + zh-CN: 集群我的 epoch + en-US: Cluster My Epoch + ja-JP: 最後にリーダー選挙を行った時点を記録するepoch値 + - field: cluster_stats_messages_ping_sent + type: 0 + i18n: + zh-CN: 集群统计消息 ping 发送 + en-US: Cluster Stats Messages Ping Sent + ja-JP: PINGメッセージの転送回数 + - field: cluster_stats_messages_pong_sent + type: 0 + i18n: + zh-CN: 集群统计消息 pong 发送 + en-US: Cluster Stats Messages Pong Sent + ja-JP: PONGメッセージの転送回数 + - field: cluster_stats_messages_sent + type: 0 + i18n: + zh-CN: 集群统计消息发送 + en-US: Cluster Stats Messages Sent + ja-JP: メッセージの転送回数 + - field: cluster_stats_messages_ping_received + type: 0 + i18n: + zh-CN: 集群统计消息 ping 接收 + en-US: Cluster Stats Messages Ping Received + ja-JP: 受信されたPINGメッセージの総数 + - field: cluster_stats_messages_pong_received + type: 0 + i18n: + zh-CN: 集群统计消息 pong 接收 + en-US: Cluster Stats Messages Pong Received + ja-JP: 受信されたPONGメッセージの総数 + - field: cluster_stats_messages_meet_received + type: 0 + i18n: + zh-CN: 集群统计消息 meet 接收 + en-US: Cluster Stats Messages Meet Received + ja-JP: 受信されたmeetメッセージの総数 + - field: cluster_stats_messages_received + type: 0 + i18n: + zh-CN: 集群统计消息接收 + en-US: Cluster Stats Messages Received + ja-JP: 受信されたメッセージの総数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: commandstats + i18n: + zh-CN: 命令统计 + en-US: Command Stats + ja-JP: コマンド統計情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 9 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: 标识 + en-US: Identity + ja-JP: ID + - field: cmdstat_set + type: 1 + i18n: + zh-CN: SET 命令统计 + en-US: SET COMMAND STAT + ja-JP: SET コマンド統計 + - field: cmdstat_get + type: 1 + i18n: + zh-CN: GET 命令统计 + en-US: GET COMMAND STAT + ja-JP: GET コマンド統計 + - field: cmdstat_setnx + type: 1 + i18n: + zh-CN: SETNX 命令统计 + en-US: SETNX COMMAND STAT + ja-JP: SETNX コマンド統計 + - field: cmdstat_hset + type: 1 + i18n: + zh-CN: HSET 命令统计 + en-US: HSET COMMAND STAT + ja-JP: HSET コマンド統計 + - field: cmdstat_hget + type: 1 + i18n: + zh-CN: HGET 命令统计 + en-US: HGET COMMAND STAT + ja-JP: HGET コマンド統計 + - field: cmdstat_lpush + type: 1 + i18n: + zh-CN: LPUSH 命令统计 + en-US: LPUSH COMMAND STAT + ja-JP: LPUSH コマンド統計 + - field: cmdstat_rpush + type: 1 + i18n: + zh-CN: RPUSH 命令统计 + en-US: RPUSH COMMAND STAT + ja-JP: RPUSH コマンド統計 + - field: cmdstat_lpop + type: 1 + i18n: + zh-CN: LPOP 命令统计 + en-US: LPOP COMMAND STAT + ja-JP: LPOP コマンド統計 + - field: cmdstat_rpop + type: 1 + i18n: + zh-CN: RPOP 命令统计 + en-US: RPOP COMMAND STAT + ja-JP: RPOP コマンド統計 + - field: cmdstat_llen + type: 1 + i18n: + zh-CN: LLEN 命令统计 + en-US: LLEN COMMAND STAT + ja-JP: LLEN コマンド統計 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + + - name: keyspace + i18n: + zh-CN: 键空间 + en-US: Keyspace + ja-JP: キー空間 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 9 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: 标识 + en-US: Identity + ja-JP: ID + - field: db0 + type: 1 + - field: db1 + type: 1 + - field: db2 + type: 1 + - field: db3 + type: 1 + - field: db4 + type: 1 + - field: db5 + type: 1 + - field: db6 + type: 1 + - field: db7 + type: 1 + - field: db8 + type: 1 + - field: db9 + type: 1 + - field: db10 + type: 1 + - field: db11 + type: 1 + - field: db12 + type: 1 + - field: db13 + type: 1 + - field: db14 + type: 1 + - field: db15 + type: 1 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-redis_sentinel.yml b/hertzbeat-manager/src/main/resources/define/app-redis_sentinel.yml new file mode 100644 index 0000000..7566f78 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-redis_sentinel.yml @@ -0,0 +1,867 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: cache +# The monitoring type eg: linux windows tomcat mysql aws... +app: redis_sentinel +# The monitoring i18n name +name: + zh-CN: Redis Sentinel + en-US: Redis Sentinel + ja-JP: Redis Sentinel +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Redis Sentinel 的通用指标进行采集监控。<br>您可以点击 “<i>新建 Redis Sentinel</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors Redis Database Sentinel's general performance metrics. You could click the "<i>New Redis Sentinel</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 Redis Sentinel 的通用指標進行采集監控。<br>您可以點擊 “<i>新建 Redis Sentinel</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は Redis Sentinel の一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Redis Sentinel</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/redis_sentinel + en-US: https://hertzbeat.apache.org/docs/help/redis_sentinel +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 26379 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value + defaultValue: 3000 + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: pattern + # name-param field display i18n name + name: + zh-CN: 模式 + en-US: Pattern + ja-JP: パターン + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: true + # default value + defaultValue: 2 + # hide-is hide this field and put it in advanced layout + hide: true + - field: enableSshTunnel + name: + zh-CN: 是否启用SSH隧道 + en-US: Enable SSH Tunnel + ja-JP: SSHトンネルの有効化 + type: boolean + required: true + hide: true + - field: sshHost + name: + zh-CN: SSH Host + en-US: SSH Host + ja-JP: SSHホスト + type: text + required: false + placeholder: 'When Enable SSH Tunnel' + hide: true + - field: sshPort + name: + zh-CN: SSH端口 + en-US: SSH Port + ja-JP: SSHポート + type: number + range: '[0,65535]' + required: false + defaultValue: 22 + placeholder: 'When Enable SSH tunnel' + hide: true + - field: sshTimeout + name: + zh-CN: SSH超时时间(ms) + en-US: SSH Timeout(ms) + ja-JP: SSHタイムアウト(ms) + type: number + required: false + range: '[400,200000]' + defaultValue: 6000 + hide: true + - field: sshUsername + name: + zh-CN: SSH用户名 + en-US: SSH Username + ja-JP: SSHユーザー名 + type: text + required: false + placeholder: 'When Enable SSH tunnel' + hide: true + - field: sshPassword + name: + zh-CN: SSH密码 + en-US: SSH Password + ja-JP: SSHパスワード + type: password + required: false + hide: true + - field: sshShareConnection + name: + zh-CN: 是否共享SSH连接 + en-US: Share SSH Connection + ja-JP: SSH接続共有 + type: boolean + required: true + defaultValue: true + hide: true + - field: sshPrivateKey + name: + zh-CN: SSH私钥 + en-US: SSH PrivateKey + ja-JP: SSH秘密鍵 + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + required: false + hide: true + - field: sshPrivateKeyPassphrase + name: + zh-CN: SSH密钥短语 + en-US: SSH PrivateKey PassPhrase + ja-JP: SSH秘密鍵フレーズ + type: password + required: false + hide: true +metrics: + - name: server + i18n: + zh-CN: 服务器 + en-US: Server + ja-JP: サーバー情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: 标识 + en-US: Identity + ja-JP: ID + - field: redis_version + type: 1 + i18n: + zh-CN: Redis 版本 + en-US: Redis Version + ja-JP: Redisバージョン + - field: redis_git_sha1 + type: 0 + i18n: + zh-CN: Redis Git SHA1 + en-US: Redis Git SHA1 + ja-JP: Redis Git SHA1 + - field: redis_git_dirty + type: 0 + i18n: + zh-CN: Redis Git Dirty + en-US: Redis Git Dirty + ja-JP: RedisサーバーのGitリポジトリ状態 + - field: redis_build_id + type: 1 + i18n: + zh-CN: Redis Build ID + en-US: Redis Build ID + ja-JP: Redis ビルド Id + - field: redis_mode + type: 1 + i18n: + zh-CN: Redis 模式 + en-US: Redis Mode + ja-JP: Redis モード + - field: os + type: 1 + i18n: + zh-CN: 操作系统 + en-US: Operating System + ja-JP: オーエス + - field: arch_bits + type: 0 + i18n: + zh-CN: 架构位数 + en-US: Architecture Bits + ja-JP: アーキテクチャ + - field: multiplexing_api + type: 1 + i18n: + zh-CN: 多路复用 API + en-US: Multiplexing API + ja-JP: IO多重化API + - field: atomicvar_api + type: 1 + i18n: + zh-CN: 原子变量 API + en-US: Atomicvar API + ja-JP: 原子操作API + - field: gcc_version + type: 1 + i18n: + zh-CN: GCC 版本 + en-US: GCC Version + ja-JP: GCC バージョン + - field: process_id + type: 0 + i18n: + zh-CN: 进程 ID + en-US: Process ID + ja-JP: プロセスID + - field: process_supervised + type: 1 + i18n: + zh-CN: 进程监控 + en-US: Process Supervised + ja-JP: プロセスの監視方法 + - field: run_id + type: 1 + i18n: + zh-CN: 运行 ID + en-US: Run ID + ja-JP: Run ID + - field: tcp_port + type: 0 + i18n: + zh-CN: TCP 端口 + en-US: TCP Port + ja-JP: TCP ポート + - field: server_time_usec + type: 0 + i18n: + zh-CN: 基于纪元的系统时间 + en-US: Server Time Usec + ja-JP: サーバーのタイムスタンプ + - field: uptime_in_seconds + type: 0 + i18n: + zh-CN: 运行时间(秒) + en-US: Uptime In Seconds + ja-JP: アップタイム(秒) + - field: uptime_in_days + type: 0 + i18n: + zh-CN: 运行时间(天) + en-US: Uptime In Days + ja-JP: アップタイム(日) + - field: hz + type: 0 + i18n: + zh-CN: 定时器频率 + en-US: Hz + ja-JP: イベントの実行頻度 + - field: configured_hz + type: 0 + i18n: + zh-CN: 配置定时器频率 + en-US: Configured Hz + ja-JP: 設定されたイベントの実行頻度 + - field: lru_clock + type: 0 + i18n: + zh-CN: LRU 时钟 + en-US: LRU Clock + ja-JP: LRU クロック + - field: executable + type: 1 + i18n: + zh-CN: 可执行文件 + en-US: Executable + ja-JP: サーバーの実行パス + - field: config_file + type: 1 + i18n: + zh-CN: 配置文件 + en-US: Config File + ja-JP: 設定されたサーバーの実行ファイル + - field: io_threads_active + type: 0 + i18n: + zh-CN: 活动 IO 线程 + en-US: IO Threads Active + ja-JP: 活動中のスレッド数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: clients + i18n: + zh-CN: 客户端 + en-US: Clients + ja-JP: クライアント情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: connected_clients + type: 0 + i18n: + zh-CN: 已连接客户端 + en-US: Connected Clients + ja-JP: 現在接続されているクライアントの数 + - field: cluster_connections + type: 0 + i18n: + zh-CN: 集群连接 + en-US: Cluster Connections + ja-JP: クラスター内の接続数 + - field: maxclients + type: 0 + i18n: + zh-CN: 最大客户端数 + en-US: Maxclients + ja-JP: 最大クライアント数 + - field: client_recent_max_input_buffer + type: 0 + i18n: + zh-CN: 客户端最近最大输入缓冲区 + en-US: Client Recent Max Input Buffer + ja-JP: クライアントの最近の最大入力バッファサイズ + - field: client_recent_max_output_buffer + type: 0 + i18n: + zh-CN: 客户端最近最大输出缓冲区 + en-US: Client Recent Max Output Buffer + ja-JP: クライアントの最近の最大出力バッファサイズ + - field: blocked_clients + type: 0 + i18n: + zh-CN: 阻塞客户端 + en-US: Blocked Clients + ja-JP: ブロックされたクライアント数 + - field: tracking_clients + type: 0 + i18n: + zh-CN: 跟踪客户端 + en-US: Tracking Clients + ja-JP: トラッキングを使用するクライアントの数 + - field: clients_in_timeout_table + type: 0 + i18n: + zh-CN: 超时表中的客户端 + en-US: Clients In Timeout Table + ja-JP: タイムアウトテーブル内のクライアント数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: stats + i18n: + zh-CN: 统计 + en-US: Stats + ja-JP: 統計情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: total_connections_received + type: 0 + i18n: + zh-CN: 收到总连接数 + en-US: Total Connections Received + ja-JP: サーバーの起動後に受信された合計接続数 + - field: total_commands_processed + type: 0 + i18n: + zh-CN: 处理总命令数 + en-US: Total Commands Processed + ja-JP: サーバーの起動後に処理した総コマンド数 + - field: instantaneous_ops_per_sec + type: 0 + i18n: + zh-CN: 瞬时每秒操作数 + en-US: Instantaneous Ops Per Sec + ja-JP: 1秒当たりに処理されたコマンドの数 + - field: total_net_input_bytes + type: 0 + i18n: + zh-CN: 总网络输入字节 + en-US: Total Net Input Bytes + ja-JP: 受信されたネットワークバイトの合計数 + - field: total_net_output_bytes + type: 0 + i18n: + zh-CN: 总网络输出字节 + en-US: Total Net Output Bytes + ja-JP: 転送されたネットワークバイトの合計数 + - field: instantaneous_input_kbps + type: 0 + i18n: + zh-CN: 瞬时的输入 kbps + en-US: Instantaneous Input Kbps + ja-JP: 瞬間入力速度(kbps) + - field: instantaneous_output_kbps + type: 0 + i18n: + zh-CN: 瞬时输出 kbps + en-US: Instantaneous Output Kbps + ja-JP: 瞬間出力速度(kbps) + - field: rejected_connections + type: 0 + i18n: + zh-CN: 拒绝连接数 + en-US: Rejected Connections + ja-JP: maxclients制限で拒否された接続数 + - field: sync_full + type: 0 + i18n: + zh-CN: 全量同步 + en-US: Sync Full + ja-JP: 完全同期の回数 + - field: sync_partial_ok + type: 0 + i18n: + zh-CN: 部分同步成功 + en-US: Sync Partial Ok + ja-JP: 部分同期の回数 + - field: sync_partial_err + type: 0 + i18n: + zh-CN: 部分同步失败 + en-US: Sync Partial Err + ja-JP: 部分同期のエラー回数 + - field: expired_keys + type: 0 + i18n: + zh-CN: 过期键 + en-US: Expired Keys + ja-JP: expireコマンドで削除されたキーの数 + - field: expired_stale_perc + type: 0 + i18n: + zh-CN: 过期key占比 + en-US: Expired Stale Perc + ja-JP: 期限切れのステイルキーの比率 + - field: expired_time_cap_reached_count + type: 0 + i18n: + zh-CN: 达到过期时间上限计数 + en-US: Expired Time Cap Reached Count + ja-JP: 満了時間の制限に達した回数 + - field: expire_cycle_cpu_milliseconds + type: 0 + i18n: + zh-CN: 过期周期 CPU 毫秒 + en-US: Expire Cycle CPU Milliseconds + ja-JP: 満了サイクルでかかったCPU時間(ms) + - field: evicted_keys + type: 0 + i18n: + zh-CN: 逐出键 + en-US: Evicted Keys + ja-JP: メモリ不足で追放されたキーの数 + - field: keyspace_hits + type: 0 + i18n: + zh-CN: 命中键 + en-US: Keyspace Hits + ja-JP: キースペースのヒット数 + - field: keyspace_misses + type: 0 + i18n: + zh-CN: 未命中键 + en-US: Keyspace Misses + ja-JP: キースペースのミス数 + - field: pubsub_channels + type: 0 + i18n: + zh-CN: 发布订阅频道 + en-US: Pubsub Channels + ja-JP: 有効になっているPub/Subチャネルに接続されているチャネルの数 + - field: pubsub_patterns + type: 0 + i18n: + zh-CN: 发布订阅模式 + en-US: Pubsub Patterns + ja-JP: 有効になっているPub/Subパターンで接続されたパターンチャネルの数 + - field: latest_fork_usec + type: 0 + i18n: + zh-CN: 最新 fork 毫秒 + en-US: Latest Fork Usec + ja-JP: 最後のフォーク作業にかかった時間 + - field: total_forks + type: 0 + i18n: + zh-CN: 总 fork 数 + en-US: Total Forks + ja-JP: 実行されたフォーク作業の合計数 + - field: migrate_cached_sockets + type: 0 + i18n: + zh-CN: 迁移缓存套接字 + en-US: Migrate Cached Sockets + ja-JP: マイグレーションされたソケット数 + - field: slave_expires_tracked_keys + type: 0 + i18n: + zh-CN: 从节点过期跟踪键 + en-US: Slave Expires Tracked Keys + ja-JP: スレーブで満了した、トラッキングされたキーの数 + - field: active_defrag_hits + type: 0 + i18n: + zh-CN: 活跃碎片整理命中 + en-US: Active Defrag Hits + ja-JP: アクティブなデフラグ操作中に発生したヒットの数 + - field: active_defrag_misses + type: 0 + i18n: + zh-CN: 活跃碎片整理未命中 + en-US: Active Defrag Misses + ja-JP: アクティブなデフラグ操作中に見逃したヒットの数 + - field: active_defrag_key_hits + type: 0 + i18n: + zh-CN: 活跃碎片整理键命中 + en-US: Active Defrag Key Hits + ja-JP: デフラグ操作でヒットしたキーの数 + - field: active_defrag_key_misses + type: 0 + i18n: + zh-CN: 活跃碎片整理键未命中 + en-US: Active Defrag Key Misses + ja-JP: デフラグ操作で見逃したキーの数 + - field: tracking_total_keys + type: 0 + i18n: + zh-CN: 跟踪键总数 + en-US: Tracking Total Keys + ja-JP: トラッキングされたキーの合計数 + - field: tracking_total_items + type: 0 + i18n: + zh-CN: 跟踪项总数 + en-US: Tracking Total Items + ja-JP: トラッキングされたアイテムの合計数 + - field: tracking_total_prefixes + type: 0 + i18n: + zh-CN: 跟踪前缀总数 + en-US: Tracking Total Prefixes + ja-JP: トラッキングされたプレフィックスの合計数 + - field: unexpected_error_replies + type: 0 + i18n: + zh-CN: 意外错误回复 + en-US: Unexpected Error Replies + ja-JP: 予期しないエラー応答の数 + - field: total_error_replies + type: 0 + i18n: + zh-CN: 总错误回复 + en-US: Total Error Replies + ja-JP: 発生したエラーの合計応答の数 + - field: dump_payload_sanitizations + type: 0 + i18n: + zh-CN: 转储有效负载深度完整性验证的总数 + en-US: Dump Payload Sanitizations + ja-JP: ダンプペイロードで実行された整理作業の数 + - field: total_reads_processed + type: 0 + i18n: + zh-CN: 总读取处理 + en-US: Total Reads Processed + ja-JP: 処理された読み取り作業の合計数 + - field: total_writes_processed + type: 0 + i18n: + zh-CN: 总写入处理 + en-US: Total Writes Processed + ja-JP: 処理された書き込み作業の合計数 + - field: io_threaded_reads_processed + type: 0 + i18n: + zh-CN: IO 线程读取处理 + en-US: Io Threaded Reads Processed + ja-JP: I/Oスレッドで処理された読み取り作業の数 + - field: io_threaded_writes_processed + type: 0 + i18n: + zh-CN: IO 线程写入处理 + en-US: Io Threaded Writes Processed + ja-JP: I/Oスレッドで処理された書き込み作業の数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: cpu + i18n: + zh-CN: CPU + en-US: CPU + ja-JP: CPU情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: used_cpu_sys + type: 0 + i18n: + zh-CN: 系统已使用 CPU + en-US: Sys CPU Used + ja-JP: システムモードで使用されたCPUの合計時間 + - field: used_cpu_user + type: 0 + i18n: + zh-CN: 用户已使用 CPU + en-US: User CPU Used + ja-JP: ユーザーモードで使用されたCPUの合計時間 + - field: used_cpu_sys_children + type: 0 + i18n: + zh-CN: Sys 子进程已使用 CPU + en-US: Sys Children CPU Used + ja-JP: 子プロセスがシステムモードで使用したCPU時間 + - field: used_cpu_user_children + type: 0 + i18n: + zh-CN: 用户子进程已使用 CPU + en-US: User Children CPU Used + ja-JP: 子プロセスがユーザーモードで使用したCPU時間 + - field: used_cpu_sys_main_thread + type: 0 + i18n: + zh-CN: 系统主线程已使用 CPU + en-US: Sys Main Thread CPU Used + ja-JP: プロセスがシステムモードで使用したCPU時間 + - field: used_cpu_user_main_thread + type: 0 + i18n: + zh-CN: 用户主线程已使用 CPU + en-US: User Main Thread CPU Used + ja-JP: プロセスがユーザーモードで使用したCPU時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ + + - name: sentinel + i18n: + zh-CN: 哨兵 + en-US: Sentinel + ja-JP: Sentinel + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 4 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: sentinel_masters + type: 0 + i18n: + zh-CN: 主节点 + en-US: Masters + ja-JP: マスターノード + - field: sentinel_tilt + type: 0 + i18n: + zh-CN: 倾斜 + en-US: Tilt + ja-JP: マスターをダウン状態としてマークする前の待機時間 + - field: sentinel_running_scripts + type: 0 + i18n: + zh-CN: 运行脚本 + en-US: Running Scripts + ja-JP: 実行するスクリプト + - field: sentinel_timedout_scripts + type: 0 + i18n: + zh-CN: 超时脚本数 + en-US: Timedout Scripts + ja-JP: タイムアウトスクリプト数 + - field: sentinel_scripts_queue_length + type: 0 + i18n: + zh-CN: 脚本队列长度 + en-US: Scripts Queue Length + ja-JP: 実行するスクリプトのキューの長さ + - field: sentinel_simulate_failure_flags + type: 0 + i18n: + zh-CN: 模拟失败标志 + en-US: Simulate Failure Flags + ja-JP: フェイルオーバー機能をテスト用のフラグ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + pattern: ^_^pattern^_^ + sshTunnel: + enable: ^_^enableSshTunnel^_^ + host: ^_^sshHost^_^ + port: ^_^sshPort^_^ + timeout: ^_^sshTimeout^_^ + username: ^_^sshUsername^_^ + password: ^_^sshPassword^_^ + privateKey: ^_^sshPrivateKey^_^ + privateKeyPassphrase: ^_^sshPrivateKeyPassphrase^_^ + shareConnection: ^_^sshShareConnection^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-registry.yml b/hertzbeat-manager/src/main/resources/define/app-registry.yml new file mode 100644 index 0000000..767f46d --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-registry.yml @@ -0,0 +1,149 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: registry +# The monitoring i18n name +name: + zh-CN: 注册中心监控 + en-US: REGISTRY MONITORS + ja-JP: レジストリ +# The description and help of this monitoring type +help: + zh-CN: HertzBeat对注册中心的相关指标进行监测。 + en-US: HertzBeat monitors related indicators of REGISTRY. + zh-TW: HertzBeat對注冊中心相關名額進行監測。 + ja-JP: HertzBeatはレジストリのメトリクスを監視します。 +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: Host + en-US: Host + ja-JP: ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + - field: discoveryClientTypeName + # name-param field display i18n name + name: + zh-CN: 注册中心类型 + en-US: Discovery Client Type + ja-JP: ディスカバリークライアントのタイプ + # type-param field type(most mapping the html input type) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: Consul + value: Consul + - label: Nacos + value: Nacos + +metrics: + - name: server + i18n: + zh-CN: 注册中心信息 + en-US: Discovery Client Info + ja-JP: ディスカバリークライアントの情報 + priority: 0 + fields: + - field: address + type: 1 + i18n: + zh-CN: 注册中心地址 + en-US: Address + ja-JP: アドレス + - field: port + type: 1 + i18n: + zh-CN: 注册中心端口 + en-US: Port + ja-JP: ポート + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + protocol: registry + registry: + host: ^_^host^_^ + port: ^_^port^_^ + discoveryClientTypeName: ^_^discoveryClientTypeName^_^ + - name: service + i18n: + zh-CN: 服务实例 + en-US: Service Instance + ja-JP: サービスインスタンス + priority: 1 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: serviceId + type: 1 + label: true + i18n: + zh-CN: 服务实例id + en-US: Service Id + ja-JP: サービスId + - field: serviceName + type: 1 + i18n: + zh-CN: 服务实例名称 + en-US: Service Name + ja-JP: サービス名 + - field: address + type: 1 + i18n: + zh-CN: 主机地址 + en-US: Address + ja-JP: アドレス + - field: port + type: 0 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: healthStatus + type: 1 + i18n: + zh-CN: 服务健康状态 + en-US: Health Status + ja-JP: 健全状態 + protocol: registry + registry: + host: ^_^host^_^ + port: ^_^port^_^ + discoveryClientTypeName: ^_^discoveryClientTypeName^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-rocketmq.yml b/hertzbeat-manager/src/main/resources/define/app-rocketmq.yml new file mode 100644 index 0000000..62e0d43 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-rocketmq.yml @@ -0,0 +1,221 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +category: mid +app: rocketmq +name: + zh-CN: RocketMQ消息系统 + en-US: RocketMQ + ja-JP: RocketMQメッセージシステム +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 RocketMQ 消息系统的通用指标进行测量监控。<br>您可以点击 “<i>新建 RocketMQ</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors RocketMQ Message System general performance metrics. You could click the "<i>New RocketMQ</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 RocketMQ 消息系統的通用指標進行測量監控。<br>您可以點擊 “<i>新建 RocketMQ</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は RocketMQメッセージシステムの一般的なメトリクスを監視します。<br>「<i>新規 RocketMQ</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/rocketmq + en-US: https://hertzbeat.apache.org/docs/help/rocketmq +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 注册中心Host + en-US: Namesrv Host + ja-JP: ネームサーバーホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + # when type is number, range is required + range: '[0,65535]' + required: true + defaultValue: 9876 + - field: accessKey + name: + zh-CN: accessKey + en-US: accessKey + ja-JP: アクセスキー + type: text + - field: secretKey + name: + zh-CN: secretKey + en-US: secretKey + ja-JP: 秘密鍵 + type: text +metrics: + - name: cluster + i18n: + zh-CN: 集群 + en-US: cluster + ja-JP: クラスター + priority: 0 + fields: + - field: BrokerId + i18n: + zh-CN: BrokerId + en-US: BrokerId + ja-JP: ブローカーId + type: 1 + - field: Address + i18n: + zh-CN: 地址 + en-US: Address + ja-JP: アドレス + type: 1 + label: true + - field: Version + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + type: 1 + - field: Producer_Message_TPS + i18n: + zh-CN: 生产消息TPS + en-US: Producer_Message_TPS + ja-JP: プロデューサーの送信速度(TPS) + type: 0 + - field: Consumer_Message_TPS + i18n: + zh-CN: 消费消息TPS + en-US: Consumer_Message_TPS + ja-JP: コンシューマーの受信速度(TPS) + type: 0 + - field: Yesterday_Produce_Count + i18n: + zh-CN: 昨天生产消息数 + en-US: Yesterday_Produce_Count + ja-JP: 昨日のプロデューサーの送信数 + type: 0 + - field: Yesterday_Consume_Count + i18n: + zh-CN: 昨天消费消息数 + en-US: Yesterday_Consume_Count + ja-JP: 昨日のコンシューマーの受信数 + type: 0 + - field: Today_Produce_Count + i18n: + zh-CN: 今天生产消息数 + en-US: Today_Produce_Count + ja-JP: 今日のプロデューサーの送信数 + type: 0 + - field: Today_Consume_Count + i18n: + zh-CN: 今天消费消息数 + en-US: Today_Consume_Count + ja-JP: 今日のコンシューマーの受信数 + type: 0 + aliasFields: + - brokerId + - address + - version + - producerMessageTps + - consumerMessageTps + - yesterdayProduceCount + - yesterdayConsumeCount + - todayProduceCount + - todayConsumeCount + calculates: + - BrokerId=brokerId + - Address=address + - Version=version + - Producer_Message_TPS=producerMessageTps + - Consumer_Message_TPS=consumerMessageTps + - Yesterday_Produce_Count=yesterdayProduceCount + - Yesterday_Consume_Count=yesterdayConsumeCount + - Today_Produce_Count=todayProduceCount + - Today_Consume_Count=todayConsumeCount + # The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, rocketmq + protocol: rocketmq + # This section describes how to configure data collection when protocol is http + rocketmq: + namesrvHost: ^_^host^_^ + namesrvPort: ^_^port^_^ + accessKey: ^_^accessKey^_^ + secretKey: ^_^secretKey^_^ + parseScript: $.clusterBrokerDataList.* + - name: consumer + i18n: + zh-CN: 消费者 + en-US: Consumer + ja-JP: コンシューマー + priority: 1 + fields: + - field: Consumer_group + i18n: + zh-CN: 消费者组 + en-US: Consumer_group + ja-JP: コンシューマーグループ + type: 1 + label: true + - field: Client_quantity + i18n: + zh-CN: 客户端数量 + en-US: Client_quantity + ja-JP: クライアント数 + type: 0 + - field: Message_model + i18n: + zh-CN: 消息模式 + en-US: Message_model + ja-JP: メッセージモデル + type: 1 + - field: Consume_type + i18n: + zh-CN: 消费类型 + en-US: Consume_type + ja-JP: 消費タイプ + type: 1 + - field: Consume_tps + i18n: + zh-CN: 消费TPS + en-US: Consume_tps + ja-JP: 受信速度(TPS) + type: 0 + - field: Delay + i18n: + zh-CN: 延迟 + en-US: Delay + ja-JP: 遅延 + type: 0 + aliasFields: + - consumerGroup + - clientQuantity + - messageModel + - consumeType + - consumeTps + - diffTotal + calculates: + - Consumer_group=consumerGroup + - Client_quantity=clientQuantity + - Message_model=messageModel + - Consume_type=consumeType + - Consume_tps=consumeTps + - Delay=diffTotal + # The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk, rocketmq + protocol: rocketmq + # This section describes how to configure data collection when protocol is http + rocketmq: + namesrvHost: ^_^host^_^ + namesrvPort: ^_^port^_^ + parseScript: $.consumerInfoList.* diff --git a/hertzbeat-manager/src/main/resources/define/app-rockylinux.yml b/hertzbeat-manager/src/main/resources/define/app-rockylinux.yml new file mode 100644 index 0000000..f683ccf --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-rockylinux.yml @@ -0,0 +1,765 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: rockylinux +# The monitoring i18n name +name: + zh-CN: Rocky Linux + en-US: Rocky Linux + ja-JP: Rocky Linux +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 Rocky 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Rocky</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors Rocky operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New Rocky</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 Rocky 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Rocky</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> Rockyオーエスの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Rocky</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/rockylinux + en-US: https://hertzbeat.apache.org/docs/help/rockylinux +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: システムバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: (uname -r ; hostname ; uptime | awk -F "," '{print $1}' | sed "s/ //g") | sed ":a;N;s/\n/^/g;ta" | awk -F '^' 'BEGIN{print "version hostname uptime"} {print $1, $2, $3}' + # ssh response data parse type: oneRow, multiRow + parseType: multiRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100-idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: "LANG=C lscpu | awk -F: '/Model name/ {print $2}' | awk 'NR==1';awk '/processor/{core++} END{print core}' /proc/cpuinfo;uptime | sed 's/,/ /g' | awk '{for(i=NF-2;i<=NF;i++)print $i }' | xargs;vmstat 1 1 | awk 'NR==3{print $11}';vmstat 1 1 | awk 'NR==3{print $12}';vmstat 1 2 | awk 'NR==4{print $15}'" + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: free -m | awk 'BEGIN{print "total used free buff_cache available"} NR==2{print $2,$3,$4,$6,$7}' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + en-US: Partition Num + ja-JP: パーティション + - field: block_write + type: 0 + i18n: + zh-CN: 写磁盘块数 + en-US: Block Write + ja-JP: 書き込みディスクブロック数 + - field: block_read + type: 0 + i18n: + zh-CN: 读磁盘块数 + en-US: Block Read + ja-JP: 読み取りブロック数 + - field: write_rate + type: 0 + unit: iops + i18n: + zh-CN: 磁盘写速率 + en-US: Write Rate + ja-JP: ディスク書き込み速度 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: vmstat -D | awk 'NR==1{print $1}';vmstat -D | awk 'NR==2{print $1}';vmstat 1 1 | awk 'NR==3{print $10}';vmstat 1 1 | awk 'NR==3{print $9}';vmstat 1 1 | awk 'NR==3{print $16}' + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: cat /proc/net/dev | tail -n +3 | awk 'BEGIN{ print "interface_name receive_bytes transmit_bytes"} {print $1,$2,$10}' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -m | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-s7.yml b/hertzbeat-manager/src/main/resources/define/app-s7.yml new file mode 100644 index 0000000..48177b6 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-s7.yml @@ -0,0 +1,275 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: s7 +# The app api i18n name +name: + zh-CN: s7服务器 + en-US: s7 Server + ja-JP: s7サーバー +# The description and help of this monitoring type +help: + zh-CN: HertzBeat对支持s7协议的服务进行(保持寄存器和线圈)相关指标进行采集 + en-US: HertzBeat collects metrics related to maintaining registers and coils for services that support s7 protocol + zh-TW: HertzBeat對支持s7協定的服務進行(保持寄存器和線圈)相關名額進行採集 + ja-JP: HertzBeatはs7プロトコルをサポートするサービスのメトリクスを監視します。 +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: s7服务Host + en-US: s7 Server Host + ja-JP: s7サーバーのホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 102 + - field: rackId + # name-param field display i18n name + name: + zh-CN: rackId + en-US: rackId + ja-JP: rackId + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + defaultValue: 0 + - field: slotId + # name-param field display i18n name + name: + zh-CN: slotId + en-US: slotId + ja-JP: slotId + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + defaultValue: 0 + - field: controllerType + # name-param field display i18n name + name: + zh-CN: controllerType + en-US: controllerType + ja-JP: controllerType + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + defaultValue: S7_1500 + - field: timeout + name: + zh-CN: 请求超时时间(ms) + en-US: Request Timeout(ms) + ja-JP: リクエストタイムアウト(ms) + type: number + # when type is number, range is required + range: '[400,200000]' + required: false + defaultValue: 6000 + hide: true + - field: holdingRegisterAddresses + # name-param field display i18n name + name: + zh-CN: 保持寄存器地址 + en-US: Holding Registers address + ja-JP: Holdingレジスタのアドレス + # type-param field type(most mapping the html input type) + type: array + # param field input placeholder + placeholder: 'Input RegisterAddress' + # required-true or false + required: true + - field: coilRegisterAddresses + # name-param field display i18n name + name: + zh-CN: 线圈寄存器地址 + en-US: Coil Register address + ja-JP: コイルレジスタのアドレス + # type-param field type(most mapping the html input type) + type: array + # param field input placeholder + placeholder: 'Input RegisterAddress' + # required-true or false + required: true + +# collect metrics config list +metrics: + # metrics - summary + - name: holding-register + i18n: + zh-CN: 保持寄存器 统计信息 + en-US: holding-register stats + ja-JP: Holdingレジスタの統計情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: address-0 + type: 0 + i18n: + zh-CN: address-0 + en-US: address-0 + ja-JP: address-0 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: address-1 + type: 0 + i18n: + zh-CN: address-1 + en-US: address-1 + ja-JP: address-1 + - field: address-2 + type: 0 + i18n: + zh-CN: address-2 + en-US: address-2 + ja-JP: address-2 + - field: address-3 + type: 0 + i18n: + zh-CN: address-3 + en-US: address-3 + ja-JP: address-3 + aliasFields: + - responseTime + - holding-register:0 + - holding-register:1 + - holding-register:2 + - holding-register:3 + # mapping and conversion expressions, use the same aliasField above to calculate metrics value# eg: cores=core1+core2, usage=usage, waitTimeallTime-runningTime + calculates: + - responseTime=responseTime + - address-0=holding-register:0 + - address-1=holding-register:1 + - address-2=holding-register:2 + - address-3=holding-register:3 + protocol: s7 + # the config content when protocol is http + s7: + # host + host: ^_^host^_^ + # port + port: ^_^port^_^ + driverName: s7 + addressSyntax: holding-register + rackId: ^_^rackId^_^ + slotId: ^_^slotId^_^ + controllerType: ^_^controllerType^_^ + timeout: ^_^timeout^_^ + registerAddresses: [ ^_^holdingRegisterAddresses^_^ ] + + # metrics - summary + - name: coil + i18n: + zh-CN: 线圈 统计信息 + en-US: coil stats + ja-JP: コイルの統計情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: address-0 + type: 0 + i18n: + zh-CN: address-0 + en-US: address-0 + ja-JP: address-0 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: address-1 + type: 0 + i18n: + zh-CN: address-1 + en-US: address-1 + ja-JP: address-1 + - field: address-2 + type: 0 + i18n: + zh-CN: address-2 + en-US: address-2 + ja-JP: address-2 + - field: address-3 + type: 0 + i18n: + zh-CN: address-3 + en-US: address-3 + ja-JP: address-3 + aliasFields: + - responseTime + - coil:0 + - coil:1 + - coil:2 + - coil:3 + # mapping and conversion expressions, use the same aliasField above to calculate metrics value# eg: cores=core1+core2, usage=usage, waitTimeallTime-runningTime + calculates: + - responseTime=responseTime + - address-0=coil:0 + - address-1=coil:1 + - address-2=coil:2 + - address-3=coil:3 + protocol: s7 + # the config content when protocol is http + s7: + # host + host: ^_^host^_^ + # port + port: ^_^port^_^ + driverName: s7 + addressSyntax: coil + rackId: ^_^rackId^_^ + slotId: ^_^slotId^_^ + controllerType: ^_^controllerType^_^ + timeout: ^_^timeout^_^ + registerAddresses: [ ^_^coilRegisterAddresses^_^ ] + + + diff --git a/hertzbeat-manager/src/main/resources/define/app-seatunnel.yml b/hertzbeat-manager/src/main/resources/define/app-seatunnel.yml new file mode 100644 index 0000000..9057952 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-seatunnel.yml @@ -0,0 +1,556 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: seatunnel +# The monitoring i18n name +name: + zh-CN: Seatunnel + en-US: Seatunnel + ja-JP: Seatunnel +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Seatunnel 通用指标进行测量监控。<br>您可以点击 “<i>新建 Seatunnel</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat measures and monitors the general metrics of Seatunnel.<br>You can click on “<i>Create New Seatunnel</i>” to configure it, or select “<i>More Actions</i>” to import an existing configuration. + zh-TW: Hertzbeat 對 Seatunnel 通用指標進行測量監控。<br>您可以點擊 “<i>新建 Seatunnel</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は Seatunnelの一般的なメトリクスを監視します。<br>「<i>新規 Seatunnel</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/seatunnel + en-US: https://hertzbeat.apache.org/docs/help/seatunnel +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 5801 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启用HTTPS + en-US: SSL + ja-JP: SSL + # type-param field type(boolean mapping the html h tag) + type: boolean + # required-true or false + required: true + +# collect metrics config list +metrics: + - name: overview + i18n: + zh-CN: 集群概览 + en-US: Cluster Overview + ja-JP: クラスター概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: projectVersion + type: 1 + i18n: + zh-CN: 项目版本 + en-US: Project Version + ja-JP: プロジェクトバージョン + - field: gitCommitAbbrev + type: 1 + i18n: + zh-CN: Git提交编号 + en-US: Git Commit Abbrev + ja-JP: GitコミットID + - field: totalSlot + type: 0 + i18n: + zh-CN: 总槽位数 + en-US: Total Slot + ja-JP: スロット数 + - field: unassignedSlot + type: 0 + i18n: + zh-CN: 未分配槽位数 + en-US: Unassigned Slot + ja-JP: 未割り当てのスロット数 + - field: runningJobs + type: 0 + i18n: + zh-CN: 运行中任务数 + en-US: Running Jobs + ja-JP: 実行中のタスク数 + - field: finishedJobs + type: 0 + i18n: + zh-CN: 已完成任务数 + en-US: Finished Jobs + ja-JP: 完了したタスク数 + - field: failedJobs + type: 0 + i18n: + zh-CN: 失败任务数 + en-US: Failed Jobs + ja-JP: 失敗したタスク数 + - field: cancelledJobs + type: 0 + i18n: + zh-CN: 取消任务数 + en-US: Cancelled Jobs + ja-JP: キャンセルされたタスク数 + - field: workers + type: 0 + i18n: + zh-CN: Worker数 + en-US: Workers + ja-JP: ワーカー数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - projectVersion + - gitCommitAbbrev + - totalSlot + - unassignedSlot + - runningJobs + - finishedJobs + - failedJobs + - cancelledJobs + - workers + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - projectVersion=projectVersion + - gitCommitAbbrev=gitCommitAbbrev + - totalSlot=totalSlot + - unassignedSlot=unassignedSlot + - runningJobs=runningJobs + - finishedJobs=finishedJobs + - failedJobs=failedJobs + - cancelledJobs=cancelledJobs + - workers=workers + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /hazelcast/rest/maps/overview + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: default + + - name: thread + i18n: + zh-CN: 线程信息 + en-US: Thread Info + ja-JP: スレッド情報 + priority: 1 + fields: + - field: threadName + type: 1 + label: true + i18n: + zh-CN: 线程名称 + en-US: Thread Name + ja-JP: スレッド名 + - field: threadId + type: 0 + i18n: + zh-CN: 线程ID + en-US: Thread ID + ja-JP: スレッドID + - field: threadState + type: 1 + i18n: + zh-CN: 线程状态 + en-US: Thread State + ja-JP: スレッド状態 + - field: stackTrace + type: 1 + i18n: + zh-CN: 堆栈信息 + en-US: Stack Trace + ja-JP: スタックトレース + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /hazelcast/rest/maps/thread-dump + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + # parse script when parse type is josnPath + parseScript: '$.*' + + - name: system_monitor + i18n: + zh-CN: 节点监控 + en-US: Node Monitor + ja-JP: ノード情報 + priority: 1 + fields: + - field: isMaster + type: 1 + label: true + i18n: + zh-CN: 是否主节点 + en-US: Is Master + ja-JP: マスターノード + - field: host + type: 1 + i18n: + zh-CN: IP地址 + en-US: IP Address + ja-JP: IPアドレス + - field: port + type: 1 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: processors + type: 1 + i18n: + zh-CN: 处理器数 + en-US: Processors + ja-JP: プロセッサ数 + - field: physical.memory.total + type: 1 + i18n: + zh-CN: 物理内存总量 + en-US: Physical Memory Total + ja-JP: 物理メモリ容量 + - field: physical.memory.free + type: 1 + i18n: + zh-CN: 物理内存可用 + en-US: Physical Memory Free + ja-JP: 利用可能な物理メモリ + - field: swap.space.total + type: 1 + i18n: + zh-CN: 交换空间总量 + en-US: Swap Space Total + ja-JP: スワップ領域容量 + - field: swap.space.free + type: 1 + i18n: + zh-CN: 交换空间可用 + en-US: Swap Space Free + ja-JP: 利用可能なスワップ領域 + - field: heap.memory.used + type: 1 + i18n: + zh-CN: 堆内存已用 + en-US: Heap Memory Used + ja-JP: 使用したヒープメモリ + - field: heap.memory.free + type: 1 + i18n: + zh-CN: 堆内存可用 + en-US: Heap Memory Free + ja-JP: 利用可能なヒープメモリ + - field: heap.memory.total + type: 1 + i18n: + zh-CN: 堆内存总量 + en-US: Heap Memory Total + ja-JP: ヒープメモリ容量 + - field: heap.memory.max + type: 1 + i18n: + zh-CN: 堆内存最大 + en-US: Heap Memory Max + ja-JP: ヒープメモリの最大値 + - field: heap.memory.used/total + type: 1 + i18n: + zh-CN: 堆内存使用率 + en-US: Heap Memory Used/Total + ja-JP: ヒープメモリの使用率 + - field: heap.memory.used/max + type: 1 + i18n: + zh-CN: 堆内存使用率最大 + en-US: Heap Memory Used/Max + ja-JP: ヒープメモリの使用率の最大値 + - field: minor.gc.count + type: 1 + i18n: + zh-CN: 小垃圾回收次数 + en-US: Minor GC Count + ja-JP: 若い領域GC回数 + - field: minor.gc.time + type: 1 + i18n: + zh-CN: 小垃圾回收时间 + en-US: Minor GC Time + ja-JP: 若い領域GC時間 + - field: major.gc.count + type: 1 + i18n: + zh-CN: 大垃圾回收次数 + en-US: Major GC Count + ja-JP: 古い領域GC回数 + - field: major.gc.time + type: 1 + i18n: + zh-CN: 大垃圾回收时间 + en-US: Major GC Time + ja-JP: 古い領域GC時間 + - field: load.process + type: 1 + i18n: + zh-CN: 进程负载 + en-US: Load Process + ja-JP: プロセスロード + - field: load.system + type: 1 + i18n: + zh-CN: 系统负载 + en-US: Load System + ja-JP: システムロード + - field: load.systemAverage + type: 1 + i18n: + zh-CN: 系统平均负载 + en-US: Load System Average + ja-JP: システムの平均ロード + - field: thread.count + type: 1 + i18n: + zh-CN: 线程数 + en-US: Thread Count + ja-JP: スレッド数 + - field: thread.peakCount + type: 1 + i18n: + zh-CN: 线程峰值 + en-US: Thread Peak Count + ja-JP: 最大スレッド数 + - field: cluster.timeDiff + type: 1 + i18n: + zh-CN: 集群时间差 + en-US: Cluster Time Diff + ja-JP: クラスター間の時差 + - field: event.q.size + type: 1 + i18n: + zh-CN: 事件队列大小 + en-US: Event Queue Size + ja-JP: イベントキューのサイズ + - field: executor.q.async.size + type: 1 + i18n: + zh-CN: 异步执行队列大小 + en-US: Executor Queue Async Size + ja-JP: 非同期実行キューのサイズ + - field: executor.q.client.size + type: 1 + i18n: + zh-CN: 客户端执行队列大小 + en-US: Executor Queue Client Size + ja-JP: クライアントが実行するキューのサイズ + - field: executor.q.client.query.size + type: 1 + i18n: + zh-CN: 客户端查询队列大小 + en-US: Executor Queue Client Query Size + ja-JP: クライアントが実行するクエリキューのサイズ + - field: executor.q.client.blocking.size + type: 1 + i18n: + zh-CN: 客户端阻塞队列大小 + en-US: Executor Queue Client Blocking Size + ja-JP: クライアントが実行するブロッキングキューのサイズ + - field: executor.q.query.size + type: 1 + i18n: + zh-CN: 查询队列大小 + en-US: Executor Queue Query Size + ja-JP: クエリキューのサイズ + - field: executor.q.scheduled.size + type: 1 + i18n: + zh-CN: 定时执行队列大小 + en-US: Executor Queue Scheduled Size + ja-JP: スケジュールされたキューのサイズ + - field: executor.q.io.size + type: 1 + i18n: + zh-CN: IO队列大小 + en-US: Executor Queue IO Size + ja-JP: IOキューのサイズ + - field: executor.q.system.size + type: 1 + i18n: + zh-CN: 系统执行队列大小 + en-US: Executor Queue System Size + ja-JP: システムキューのサイズ + - field: executor.q.operations.size + type: 1 + i18n: + zh-CN: 操作队列大小 + en-US: Executor Queue Operations Size + ja-JP: 操作キューのサイズ + - field: executor.q.priorityOperation.size + type: 1 + i18n: + zh-CN: 优先操作队列大小 + en-US: Executor Queue Priority Operation Size + ja-JP: 優先操作キューのサイズ + - field: operations.completed.count + type: 1 + i18n: + zh-CN: 完成操作次数 + en-US: Operations Completed Count + ja-JP: 操作完了の回数 + - field: executor.q.mapLoad.size + type: 1 + i18n: + zh-CN: 映射加载队列大小 + en-US: Executor Queue Map Load Size + ja-JP: Mapからロードするキューのサイズ + - field: executor.q.mapLoadAllKeys.size + type: 1 + i18n: + zh-CN: 映射加载所有键大小 + en-US: Executor Queue Map Load All Keys Size + ja-JP: Mapから全てのキーをロードするキューのサイズ + - field: executor.q.cluster.size + type: 1 + i18n: + zh-CN: 集群执行队列大小 + en-US: Executor Queue Cluster Size + ja-JP: クラスターキューのサイズ + - field: executor.q.response.size + type: 1 + i18n: + zh-CN: 响应队列大小 + en-US: Executor Queue Response Size + ja-JP: 応答キューのサイズ + - field: operations.running.count + type: 1 + i18n: + zh-CN: 正在运行的操作数量 + en-US: Operations Running Count + ja-JP: 実行中のキューのサイズ + - field: operations.pending.invocations.percentage + type: 1 + i18n: + zh-CN: 待处理调用百分比 + en-US: Operations Pending Invocations Percentage + ja-JP: 待機中の操作の比率 + - field: operations.pending.invocations.count + type: 1 + i18n: + zh-CN: 待处理调用数量 + en-US: Operations Pending Invocations Count + ja-JP: 待機中の操作数 + - field: proxy.count + type: 1 + i18n: + zh-CN: 代理数量 + en-US: Proxy Count + ja-JP: プロキシ数 + - field: clientEndpoint.count + type: 1 + i18n: + zh-CN: 客户端端点数量 + en-US: Client Endpoint Count + ja-JP: クライアントエンドポイントの数 + - field: connection.active.count + type: 1 + i18n: + zh-CN: 活动连接数 + en-US: Connection Active Count + ja-JP: アクティブな接続数 + - field: client.connection.count + type: 1 + i18n: + zh-CN: 客户端连接数 + en-US: Client Connection Count + ja-JP: クライアント接続数 + - field: connection.count + type: 1 + i18n: + zh-CN: 总连接数 + en-US: Connection Count + ja-JP: 接続総数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /hazelcast/rest/maps/system-monitoring-information + method: GET + ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.*' diff --git a/hertzbeat-manager/src/main/resources/define/app-shenyu.yml b/hertzbeat-manager/src/main/resources/define/app-shenyu.yml new file mode 100644 index 0000000..e7a4ace --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-shenyu.yml @@ -0,0 +1,392 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +category: mid +app: shenyu +name: + zh-CN: Apache ShenYu网关 + en-US: Apache ShenYu + ja-JP: Apache ShenYu +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 通过调用 <a href="https://shenyu.apache.org/zh/docs/plugin-center/observability/metrics-plugin">ShenYu Metrics 插件</a> 对 ShenYu 网关的通用指标 (请求量、执行时间、JVM、内存占用、线程信息等) 进行采集监控。<br>注意⚠️:需要您提前 <a href="https://shenyu.apache.org/zh/docs/plugin-center/observability/metrics-plugin">开启暴露 ShenYu Metrics 插件暴露其数据接口</a>。您可以点击 “<i>新建 ShenYu</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors metrics of the ShenYu Gateway by calling the <a href="https://shenyu.apache.org/docs/plugin-center/observability/metrics-plugin">ShenYu Metrics Api</a>. <br>Note⚠️:You need to <a href="https://shenyu.apache.org/docs/plugin-center/observability/metrics-plugin">enable ShenYu Metrics Plugin</a> in advance. You can click "<i>New ShenYu</i>" and configure it, or select "<i>More Action</i>" to import the existing configuration. + zh-TW: Hertzbeat 通過調用 <a href="https://shenyu.apache.org/zh/docs/plugin-center/observability/metrics-plugin">ShenYu Metrics 插件</a> 對 ShenYu 網關的通用指標 (請求量、執行時間、JVM、內存占用、線程信息等) 進行采集監控。<br>注意⚠️:需要您提前 <a href="https://shenyu.apache.org/zh/docs/plugin-center/observability/metrics-plugin">開啓暴露 ShenYu Metrics 插件暴露其數據接口</a>。您可以點擊 “<i>新建 ShenYu</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a href="https://shenyu.apache.org/docs/plugin-center/observability/metrics-plugin">ShenYu Metrics プラグイン</a> を呼び出して Apache ShenYu の一般的なメトリクスを監視します。<br>注意⚠️:<a href="https://shenyu.apache.org/docs/plugin-center/observability/metrics-plugin">ShenYu Metrics プラグイン を有効にする必要があります</a>。<br>「<i>新規 Apache ShenYu</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/shenyu + en-US: https://hertzbeat.apache.org/docs/help/shenyu +params: + - field: host + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + defaultValue: 8090 + required: true + - field: timeout + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + type: number + required: false + hide: true + defaultValue: 6000 +metrics: + - name: shenyu_request_total + i18n: + zh-CN: ShenYu请求总量 + en-US: ShenYu Request Total + ja-JP: ShenYuリクエスト総数 + priority: 0 + fields: + - field: value + type: 0 + # Monitoring and collection usage protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: shenyu_request_throw_created + i18n: + zh-CN: ShenYu请求异常总量 + en-US: ShenYu Request Exception Total + ja-JP: ShenYuリクエストの異常総数 + priority: 1 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: value + type: 0 + # Monitoring and collection usage protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: process_cpu_seconds_total + i18n: + zh-CN: 进程 CPU 时间总量 (秒) + en-US: Process CPU Time Total + ja-JP: プロセスのシステム時間合計 + priority: 2 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: value + type: 0 + unit: s + # Monitoring and collection usage protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: process_open_fds + i18n: + zh-CN: 进程打开的文件描述符数量 + en-US: Process Open Fds + ja-JP: プロセスが開かれたファイル記述子の数量 + priority: 3 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: value + type: 0 + # Monitoring and collection usage protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: process_max_fds + i18n: + zh-CN: 进程最大文件描述符数量 + en-US: Process Max Fds + ja-JP: プロセスが開かれたファイル記述子の最大値 + priority: 4 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: value + type: 0 + # Monitoring and collection usage protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: jvm_info + i18n: + zh-CN: JVM 信息 + en-US: JVM Info + ja-JP: Java仮想マシン情報 + priority: 5 + fields: + # Metric information includes field name, type field type (0-number number, 1-string string), label-whether it is a metric label field, unit: metric unit + - field: runtime + type: 1 + i18n: + zh-CN: 运行时 + en-US: Runtime + ja-JP: ランタイム + - field: vendor + type: 1 + i18n: + zh-CN: 供应商 + en-US: Vendor + ja-JP: ベンダー + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + # Monitoring and collection usage protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: jvm_memory_bytes_used + i18n: + zh-CN: JVM 内存使用字节数 + en-US: JVM Memory Bytes Used + ja-JP: Java仮想マシンメモリの使用したバイト数 + priority: 6 + fields: + # Metric information includes field name, type field type (0-number number, 1-string string), label-whether it is a metric label field, unit: metric unit + - field: area + type: 1 + label: true + i18n: + zh-CN: 区域 + en-US: Area + ja-JP: エリア + - field: value + type: 0 + unit: MB + units: + - value=B->MB + # Monitoring and collection usage protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: jvm_memory_pool_bytes_used + i18n: + zh-CN: JVM 内存池使用字节数 + en-US: JVM Memory Pool Bytes Used + ja-JP: Java仮想マシンメモリプールの使用したバイト数 + priority: 7 + fields: + # Metric information includes field name, type field type (0-number number, 1-string string), label-whether it is a metric label field, unit: metric unit + - field: pool + type: 1 + label: true + i18n: + zh-CN: 池 + en-US: Pool + ja-JP: プール + - field: value + type: 0 + unit: MB + units: + - value=B->MB + # Monitoring and collection usage protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: jvm_memory_pool_bytes_committed + i18n: + zh-CN: JVM 内存池提交字节数 + en-US: JVM Memory Pool Bytes Committed + ja-JP: Java仮想マシンメモリプールのコミットしたバイト数 + priority: 8 + fields: + # Metric information includes field name, type field type (0-number number, 1-string string), label-whether it is a metric label field, unit: metric unit + - field: pool + type: 1 + label: true + i18n: + zh-CN: 池 + en-US: Pool + ja-JP: プール + - field: value + type: 0 + unit: MB + units: + - value=B->MB + # Monitoring and collection usage protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: jvm_memory_pool_bytes_max + i18n: + zh-CN: JVM 内存池最大字节数 + en-US: JVM Memory Pool Bytes Max + ja-JP: Java仮想マシンメモリプールのバイト数の最大値 + priority: 9 + fields: + # Metric information includes field name, type field type (0-number number, 1-string string), label-whether it is a metric label field, unit: metric unit + - field: pool + type: 1 + label: true + i18n: + zh-CN: 池 + en-US: Pool + ja-JP: プール + - field: value + type: 0 + unit: MB + units: + - value=B->MB + # Monitoring and collection usage protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: jvm_threads_state + i18n: + zh-CN: JVM 线程状态 + en-US: JVM Threads State + ja-JP: Java仮想マシンのスレッド状態 + priority: 10 + fields: + # Metric information includes field name, type field type (0-number number, 1-string string), label-whether it is a metric label field, unit: metric unit + - field: state + type: 1 + label: true + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + - field: count + type: 0 + i18n: + zh-CN: 数量 + en-US: Count + ja-JP: 数量 + aliasFields: + - state + - value + calculates: + - count=value + # Monitoring and collection usage protocol eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # This section describes how to configure data collection when protocol is http + http: + # Host: ipv4 ipv6 domain name + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: '' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus diff --git a/hertzbeat-manager/src/main/resources/define/app-smtp.yml b/hertzbeat-manager/src/main/resources/define/app-smtp.yml new file mode 100644 index 0000000..cc339ab --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-smtp.yml @@ -0,0 +1,133 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: smtp +# The app api i18n name +name: + zh-CN: SMTP邮件服务器 + en-US: SMTP Email Server + ja-JP: SMTPメールサーバー +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 SMTP 服务的(Banner,helo响应)相关指标进行监测。<br>您可以点击 “<i>新建 SMTP邮件服务器</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat monitors the banner and hello response related metrics of the SMTP service.<br>You can click "<i>New SMTP Email Server</i>" and configure it, or select "<i>More Action</i>" to import the existing configuration. + zh-TW: HertzBeat對SMTP服務的(Banner,helo響應)相關名額進行監測。<br>您可以點擊 “<i>新建 SMTP邮件服务器</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: HertzBeat は SMTPメールサーバーの一般的なメトリクスを監視します。<br>「<i>新規 SMTPメールサーバー</i>」をクリックしてパラメタを設定した後、新規することができます。 +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: SMTP的Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value 25 + defaultValue: 25 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 连接超时时间(ms) + en-US: Connect Timeout(ms) + ja-JP: 接続タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 + # field-param field key + - field: email + name: + zh-CN: email + en-US: email + ja-JP: 電子メール + type: text + # required-true or false + required: true +# collect metrics config list +metrics: + # metrics - summary + - name: summary + i18n: + zh-CN: 概要 + en-US: Summary + ja-JP: 概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: response + type: 1 + i18n: + zh-CN: 响应状态 + en-US: Response + ja-JP: 応答状態 + - field: smtpBanner + type: 1 + i18n: + zh-CN: SMTP Banner + en-US: SMTP Banner + ja-JP: SMTP バナー + - field: heloInfo + type: 1 + i18n: + zh-CN: Helo日志 + en-US: HeloInfo + ja-JP: Helo情報 + + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: smtp + # Specific collection configuration when protocol is telnet protocol + smtp: + # telnet host + host: ^_^host^_^ + # port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # email + email: ^_^email^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-spark.yml b/hertzbeat-manager/src/main/resources/define/app-spark.yml new file mode 100644 index 0000000..e76b550 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-spark.yml @@ -0,0 +1,376 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: spark +# The monitoring i18n name +name: + zh-CN: Apache Spark + en-US: Apache Spark + ja-JP: Apache Spark +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a href="https://hertzbeat.apache.org/docs/advanced/extend-jmx">JMX 协议</a> 对 Apache Spark 的通用性能指标进行采集监控。注意⚠️:为了开启 JMX 服务,您需要在系统IP启动 server 时添加 Spark 的 VM options 选项。Master 和 Worker 的监控在 spark 集群运行时即可监控,Driver 和 Excutor 的监控需要针对某一个 app 来进行监控。请 <a href="https://hertzbeat.apache.org/zh-cn/docs/help/spark/#%E7%AC%AC%E4%B8%80%E6%AD%A5">点击查看配置步骤</a>。 + en-US: HertzBeat uses <a href="https://hertzbeat.apache.org/docs/advanced/extend-jmx">JMX protocol </a> to monitor general performance metric of Apache Spark. Note ⚠️:You should add Spark VM options When Start Server with customIP to enable the JVM services, You need to add Spark's VM options option when the system IP starts the server. <br>The monitoring of Master and Worker can be done then the Spark cluster is running, while the monitoring of Driver and Exciter needs to be targeted at a specific app. <a href='https://hertzbeat.apache.org/docs/help/spark/#%E7%AC%AC%E4%B8%80%E6%AD%A5'>Please click here to view the configuration steps</a> + zh-TW: HertzBeat 使用 <a href="https://hertzbeat.apache.org/docs/advanced/extend-jmx">JMX 协议</a> 對 Apache Spark 的通用性能指標進行採集監控。注意⚠️:為了開啟 JMX 服務,您需要在系統IP啟動 server 時添加 Spark 的 VM options 選項。Master 和 Worker 的監控在 spark 集羣運行時即可監控,Driver 和 Excutor 的監控需要針對某一個 app 來進行監控。請 <a href="https://hertzbeat.apache.org/zh-cn/docs/help/spark/#%E7%AC%AC%E4%B8%80%E6%AD%A5">點擊查看配寘步驟</a>。 + ja-JP: HertzBeat は <a href="https://hertzbeat.apache.org/docs/advanced/extend-jmx"> JMXプロトコルを介して</a> Apache Spark の一般的なパフォーマンスのメトリクスを監視します。⚠️注意:Hadoop で Sparkの仮想マシンのオプション を有効にする必要があります。<a href="https://hertzbeat.apache.org/docs/help/spark/#%E7%AC%AC%E4%B8%80%E6%AD%A5">クリックしてガイドを見ます</a>。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/spark + en-US: https://hertzbeat.apache.org/docs/help/spark +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 8712 + # field-param field key + - field: url + # name-param field display i18n name + name: + zh-CN: JMX URL + en-US: JMX URL + ja-JP: JMX URL + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + # param field input placeholder + placeholder: 'service:jmx:rmi:///jndi/rmi://host:port/jmxrmi' + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 虚拟机基础信息 + en-US: JVM Basic + ja-JP: Java仮想マシンの基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: VmName + type: 1 + i18n: + zh-CN: 名称 + en-US: Vm Name + ja-JP: Java仮想マシン名 + - field: VmVendor + type: 1 + i18n: + zh-CN: 厂商 + en-US: Vm Vendor + ja-JP: Java仮想マシンのベンダー + - field: VmVersion + type: 1 + i18n: + zh-CN: 版本 + en-US: Vm Version + ja-JP: Java仮想マシンのバージョン + - field: Uptime + type: 0 + unit: ms + i18n: + zh-CN: 运行时长 + en-US: Up time + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jmx + # the config content when protocol is jmx + jmx: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + # jmx mbean object name + objectName: java.lang:type=Runtime + url: ^_^url^_^ + + - name: memory_pool + priority: 1 + i18n: + zh-CN: 内存池 + en-US: Memory Pool + ja-JP: メモリプール + fields: + - field: name + type: 1 + label: true + i18n: + zh-CN: 指标名称 + en-US: Name + ja-JP: 名前 + - field: committed + type: 0 + unit: MB + i18n: + zh-CN: 已分配内存 + en-US: Committed + ja-JP: コミットされたメモリ + - field: init + type: 0 + unit: MB + i18n: + zh-CN: 初始化内存 + en-US: Init + ja-JP: イニシャルメモリ + - field: max + type: 0 + unit: MB + i18n: + zh-CN: 最大内存 + en-US: Max + ja-JP: 最大メモリ + - field: used + type: 0 + unit: MB + i18n: + zh-CN: 已使用内存 + en-US: Used + ja-JP: 使用したメモリ + units: + - committed=B->MB + - init=B->MB + - max=B->MB + - used=B->MB + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - Name + - Usage->committed + - Usage->init + - Usage->max + - Usage->used + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - name=Name + - committed=Usage->committed + - init=Usage->init + - max=Usage->max + - used=Usage->used + protocol: jmx + jmx: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=MemoryPool,name=* + url: ^_^url^_^ + + - name: code_cache + priority: 2 + i18n: + zh-CN: 本地代码缓冲区 + en-US: Code Cache + ja-JP: コードキャッシュ + fields: + - field: committed + type: 0 + i18n: + zh-CN: 已分配内存 + en-US: Committed + ja-JP: コミットされたメモリ + - field: init + type: 0 + i18n: + zh-CN: 初始化内存 + en-US: Init + ja-JP: イニシャルメモリ + - field: max + type: 0 + i18n: + zh-CN: 最大内存 + en-US: Max + ja-JP: 最大メモリ + - field: used + type: 0 + i18n: + zh-CN: 已使用内存 + en-US: Used + ja-JP: 使用したメモリ + aliasFields: + - Usage->committed + - Usage->init + - Usage->max + - Usage->used + calculates: + - committed=Usage->committed + - init=Usage->init + - max=Usage->max + - used=Usage->used + protocol: jmx + jmx: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=MemoryPool,name=Code Cache + url: ^_^url^_^ + + - name: class_loading + priority: 3 + i18n: + zh-CN: 类加载信息 + en-US: Class Loading + ja-JP: クラスローディング情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: LoadedClassCount + type: 0 + i18n: + zh-CN: 当前已加载类数量 + en-US: Loaded Class Count + ja-JP: ロードされたクラス数 + - field: TotalLoadedClassCount + type: 0 + i18n: + zh-CN: 已加载类总数量 + en-US: Total Loaded Class Count + ja-JP: ロードされたクラス総数 + - field: UnloadedClassCount + type: 0 + i18n: + zh-CN: 未加载类总数量 + en-US: Unloaded Class Count + ja-JP: アンロードされたクラス総数 + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=ClassLoading + url: ^_^url^_^ + + - name: thread + priority: 4 + i18n: + zh-CN: 线程信息 + en-US: Thread + ja-JP: スレッド情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: TotalStartedThreadCount + type: 0 + i18n: + zh-CN: 已启动线程总数 + en-US: Total Started Thread Count + ja-JP: スレッド総数 + - field: ThreadCount + type: 0 + i18n: + zh-CN: 活跃线程数 + en-US: Thread Count + ja-JP: 活躍スレッド数 + - field: PeakThreadCount + type: 0 + i18n: + zh-CN: 最大峰值线程数 + en-US: Peak Thread Count + ja-JP: 最大スレッド数 + - field: DaemonThreadCount + type: 0 + i18n: + zh-CN: 活跃守护线程数 + en-US: Daemon Thread Count + ja-JP: デーモンスレッド数 + - field: CurrentThreadUserTime + type: 0 + unit: s + i18n: + zh-CN: 线程占用的CPU时间(用户态) + en-US: Current Thread User Time + ja-JP: 現在のスレッドユーザー時間 + - field: CurrentThreadCpuTime + type: 0 + unit: s + i18n: + zh-CN: 线程占用的CPU时间 + en-US: Current Thread CPU Time + ja-JP: 現在のスレッドシステム時間 + units: + - CurrentThreadUserTime=NS->S + - CurrentThreadCpuTime=NS->S + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=Threading + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-spring_gateway.yml b/hertzbeat-manager/src/main/resources/define/app-spring_gateway.yml new file mode 100644 index 0000000..2ae6324 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-spring_gateway.yml @@ -0,0 +1,489 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: mid +# The monitoring type eg: springboot2 linux windows tomcat mysql aws... +app: spring_gateway +# The monitoring i18n name +name: + zh-CN: Spring Cloud Gateway + en-US: Spring Cloud Gateway + ja-JP: Spring Cloud Gateway +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 <a class='help_module_content' href='https://www.tutorialspoint.com/spring_boot/spring_boot_actuator.htm'> SpringBoot Actuator </a> 暴露的通用性能指标(globalfilters、routefilters、refresh、routes)进行采集监控。<span class='help_module_span'>注意⚠️:如果要监控 Spring Cloud Gateway 中的信息,需要您的 Spring Cloud Gateway 应用集成并开启 SpringBoot Actuator, <a class='help_module_content' href='https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html'>点击查看具体步骤</a>。</span> + en-US: HertzBeat collect and monitors SpringGateway through general performance metric that exposed by the SpringBoot Actuator. <br><span class='help_module_span'><br>Note⚠️:You should make sure that your Spring Cloud Gateway application have already integrated and enabled the SpringBoot Actuator, <a class='help_module_content' href='https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html'>click here to see the specific steps.</a></span> + zh-TW: HertzBeat 對 <a class='help_module_content' href='https://www.tutorialspoint.com/spring_boot/spring_boot_actuator.htm'> SpringBoot Actuator </a>暴露的通用性能指標(globalfilters、routefilters、refresh、routes)進行採集監控。<span class='help_module_span'>注意⚠️:如果要監控 Spring Cloud Gateway 中的指標,需要您的 Spring Cloud Gateway 應用集成並開啟SpringBoot Actuator,<a class='help_module_content' href='https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html'>點擊查看具體步驟</a>。</span> + ja-JP: HertzBeat は <a class='help_module_content' href='https://www.tutorialspoint.com/spring_boot/spring_boot_actuator.htm'> SpringBoot Actuator </a> の一般的なパフォーマンスのメトリクスを監視します。<span class='help_module_span'>⚠️注意:Spring Cloud Gateway で SpringBoot Actuatorを有効にする必要があります。<a class='help_module_content' href='https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/spring_gateway + en-US: https://hertzbeat.apache.org/docs/help/spring_gateway +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 8008 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL + # When the type is boolean, the frontend will display a switch for it. + type: boolean + # required-true or false + required: false + # field-param field key + - field: base_path + # name-param field display i18n name + name: + zh-CN: Base Path + en-US: Base Path + ja-JP: Base Path + # type-param field type(most mapping the html input type) The type "text" belongs to a text input field. + type: text + # default value + defaultValue: /actuator + # required-true or false + required: true + # hide-true or false + hide: true + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: Basic Auth Username + en-US: Basic Auth Username + ja-JP: 認証ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: Basic Auth Password + en-US: Basic Auth Password + ja-JP: 認証パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - available + - name: available + i18n: + zh-CN: 可用性 + en-US: Availability + ja-JP: 可用性 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), label-is label primary key, unit-metric unit + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^base_path^_^ + # http request method GET POST PUT DELETE PATCH + method: GET + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-api availability monitoring + parseType: default + + - name: environment + i18n: + zh-CN: 环境 + en-US: Environment + ja-JP: 環境情報 + priority: 1 + fields: + # The metric information, including field name, type of the field (0-number, 1-string), whether it is an instance primary key, and the unit of the metric. + - field: profile + type: 1 + i18n: + zh-CN: 配置文件 + en-US: Profile + ja-JP: プロフィール + - field: port + type: 0 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: os + type: 1 + i18n: + zh-CN: 操作系统 + en-US: Operating System + ja-JP: オーエス + - field: os_arch + type: 1 + i18n: + zh-CN: 操作系统架构 + en-US: Operating System Architecture + ja-JP: オーエスアーキテクチャ + - field: jdk_vendor + type: 1 + i18n: + zh-CN: JDK 供应商 + en-US: JDK Vendor + ja-JP: JDK ベンダー + - field: jvm_version + type: 1 + i18n: + zh-CN: JVM 版本 + en-US: JVM Version + ja-JP: Java仮想マシンバージョン + # metric alias list, used to identify metrics in query results + aliasFields: + - $.activeProfiles[0] + - $.propertySources[?(@.name=='server.ports')].properties.*.value + - $.propertySources[?(@.name=='systemProperties')].properties['os.name'].value + - $.propertySources[?(@.name=='systemProperties')].properties['jdk.vendor.version'].value + - $.propertySources[?(@.name=='systemProperties')].properties['java.runtime.version'].value + - $.propertySources[?(@.name=='systemProperties')].properties['os.arch'].value + # A list of calculation scripts for metric values. + calculates: + - profile=$.activeProfiles[0] + - port=$.propertySources[?(@.name=='server.ports')].properties.*.value + - os=$.propertySources[?(@.name=='systemProperties')].properties['os.name'].value + - jdk_vendor=$.propertySources[?(@.name=='systemProperties')].properties['jdk.vendor.version'].value + - jvm_version=$.propertySources[?(@.name=='systemProperties')].properties['java.runtime.version'].value + - os_arch=$.propertySources[?(@.name=='systemProperties')].properties['os.arch'].value + # The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^base_path^_^/env + # http request method GET POST PUT DELETE PATCH + method: GET + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-api availability monitoring + parseType: jsonPath + # http response data parse script + parseScript: '$' + + - name: thread_state + i18n: + zh-CN: 线程状态 + en-US: Thread State + ja-JP: スレッド状態 + visible: false + priority: 2 + fields: + - field: state + type: 1 + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/metrics/jvm.threads.states + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.availableTags[?(@.tag == "state")].values[*]' + + - name: threads + i18n: + zh-CN: 线程 + en-US: Threads + ja-JP: スレッド情報 + priority: 3 + fields: + - field: state + type: 1 + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + label: true + - field: Size + type: 0 + i18n: + zh-CN: 数量 + en-US: Size + ja-JP: 数量 + aliasFields: + - $.measurements[?(@.statistic == "VALUE")].value + calculates: + - state='^o^state^o^' + - Size=$.measurements[?(@.statistic == "VALUE")].value + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/metrics/jvm.threads.states?tag=state:^o^state^o^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$' + + - name: space_name + i18n: + zh-CN: 空间名称 + en-US: Space Name + ja-JP: スペース名 + visible: false + priority: 4 + fields: + - field: id + type: 1 + i18n: + zh-CN: ID + en-US: ID + ja-JP: ID + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/metrics/jvm.memory.used + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.availableTags[?(@.tag == "id")].values[*]' + + - name: memory_used + i18n: + zh-CN: 内存使用 + en-US: Memory Used + ja-JP: メモリ情報 + priority: 5 + fields: + - field: space + type: 1 + i18n: + zh-CN: 空间 + en-US: Space + ja-JP: スペース + label: true + - field: mem_used + type: 0 + i18n: + zh-CN: 内存使用 + en-US: Memory Used + ja-JP: 使用したメモリ + unit: MB + aliasFields: + - $.measurements[?(@.statistic == "VALUE")].value + calculates: + - space="^o^id^o^" + - mem_used=$.measurements[?(@.statistic == "VALUE")].value + units: + - mem_used=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/metrics/jvm.memory.used?tag=id:^o^id^o^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$' + + - name: health + i18n: + zh-CN: 健康 + en-US: Health + ja-JP: 健全情報 + priority: 6 + fields: + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: 状態 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/health + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: default + + - name: route_id + i18n: + zh-CN: 路由 ID + en-US: Route ID + ja-JP: ルート ID + visible: false + priority: 7 + fields: + - field: name + type: 1 + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/gateway/routes + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$[*].route_id' + + - name: route_info + i18n: + zh-CN: 路由信息 + en-US: Route Info + ja-JP: ルート情報 + priority: 8 + fields: + - field: route_id + type: 1 + i18n: + zh-CN: 路由 ID + en-US: Route ID + ja-JP: ルート ID + label: true + - field: predicate + type: 1 + i18n: + zh-CN: 断言 + en-US: Predicate + ja-JP: 述語 + - field: uri + type: 1 + i18n: + zh-CN: URI + en-US: URI + ja-JP: URI + - field: order + type: 0 + i18n: + zh-CN: 顺序 + en-US: Order + ja-JP: オーダー + aliasFields: + - $.predicate + - $.uri + - $.order + calculates: + - route_id="^o^name^o^" + - predicate=$.predicate + - uri=$.uri + - order=$.order + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/gateway/routes/^o^name^o^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$' diff --git a/hertzbeat-manager/src/main/resources/define/app-springboot2.yml b/hertzbeat-manager/src/main/resources/define/app-springboot2.yml new file mode 100644 index 0000000..407cf20 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-springboot2.yml @@ -0,0 +1,418 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: program +# The monitoring type eg: springboot2 linux windows tomcat mysql aws... +app: springboot2 +# The monitoring i18n name +name: + zh-CN: SpringBoot2.0 + en-US: SpringBoot2.0 + ja-JP: SpringBoot2.0 +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 <a class='help_module_content' href='https://www.tutorialspoint.com/spring_boot/spring_boot_actuator.htm'> SpringBoot2.0 Actuator </a> 暴露的通用性能指标(health、environment、threads、memory_used)进行采集监控。<span class='help_module_span'>注意⚠️:如果要监控 SpringBoot 中的信息,需要您的 SpringBoot 应用集成并开启 SpringBoot Actuator, <a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/springboot2'>点击查看具体步骤</a>。</span> + en-US: HertzBeat collects and monitors SpringBoot through general performance metric(health, environment, threads, memory_used) that exposed by the SpringBoot2.0 Actuator. <br><span class='help_module_span'>Note⚠️:You should make sure that your SpringBoot application have already integrated and enabled the SpringBoot Actuator, <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/springboot2'>click here to see the specific steps.</a></span> + zh-TW: HertzBeat 對 <a class='help_module_content' href='https://www.tutorialspoint.com/spring_boot/spring_boot_actuator.htm'> SpringBoot2.0 Actuator </a>暴露的通用性能指標(health、environment、threads、memory_used)進行採集監控。<span class='help_module_span'>注意⚠️:如果要監控SpringBoot中的指標,需要您的SpringBoot應用集成並開啟SpringBoot Actuator,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/springboot2'>點擊查看具體步驟</a>。</span> + ja-JP: HertzBeat は <a class='help_module_content' href='https://www.tutorialspoint.com/spring_boot/spring_boot_actuator.htm'> SpringBoot2.0 Actuator </a> の一般的なパフォーマンスのメトリクスを監視します。<span class='help_module_span'>⚠️注意:SpringBoot で SpringBoot Actuatorを有効にする必要があります。<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/springboot2'>クリックしてガイドを見ます</a>。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/springboot2 + en-US: https://hertzbeat.apache.org/docs/help/springboot2 +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 8080 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL + # When the type is boolean, the frontend will display a switch for it. + type: boolean + # required-true or false + required: false + # field-param field key + - field: base_path + # name-param field display i18n name + name: + zh-CN: Base Path + en-US: Base Path + ja-JP: Base Path + # type-param field type(most mapping the html input type) The type "text" belongs to a text input field. + type: text + # default value + defaultValue: /actuator + # required-true or false + required: true + # hide-true or false + hide: true + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: Basic Auth Username + en-US: Basic Auth Username + ja-JP: 認証ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: Basic Auth Password + en-US: Basic Auth Password + ja-JP: 認証パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - available + - name: available + i18n: + zh-CN: 可用性 + en-US: Availability + ja-JP: 可用性 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^base_path^_^ + # http request method GET POST PUT DELETE PATCH + method: GET + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-api availability monitoring + parseType: default + # metrics - environment + - name: environment + i18n: + zh-CN: 环境信息 + en-US: Environment + ja-JP: 環境情報 + priority: 1 + # The specific monitoring metrics in the metric group. + fields: + # The metric information, including field name, type of the field (0-number, 1-string), whether it is an instance primary key, and the unit of the metric. + - field: profile + type: 1 + i18n: + zh-CN: 配置文件 + en-US: Profile + ja-JP: プロフィール + - field: port + type: 0 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: os + type: 1 + i18n: + zh-CN: 操作系统 + en-US: Operating System + ja-JP: オーエス + - field: os_arch + type: 1 + i18n: + zh-CN: 操作系统架构 + en-US: Operating System Architecture + ja-JP: オーエスアーキテクチャ + - field: jdk_vendor + type: 1 + i18n: + zh-CN: JDK 厂商 + en-US: JDK Vendor + ja-JP: JDK ベンダー + - field: jvm_version + type: 1 + i18n: + zh-CN: JVM 版本 + en-US: JVM Version + ja-JP: Java仮想マシンバージョン + # metric alias list, used to identify metrics in query results + aliasFields: + - $.activeProfiles + - $.propertySources[?(@.name=='server.ports')].properties.*.value + - $.propertySources[?(@.name=='systemProperties')].properties['os.name'].value + - $.propertySources[?(@.name=='systemProperties')].properties['jdk.vendor.version'].value + - $.propertySources[?(@.name=='systemProperties')].properties['java.runtime.version'].value + - $.propertySources[?(@.name=='systemProperties')].properties['os.arch'].value + # A list of calculation scripts for metric values. + calculates: + - profile=$.activeProfiles + - port=$.propertySources[?(@.name=='server.ports')].properties.*.value + - os=$.propertySources[?(@.name=='systemProperties')].properties['os.name'].value + - jdk_vendor=$.propertySources[?(@.name=='systemProperties')].properties['jdk.vendor.version'].value + - jvm_version=$.propertySources[?(@.name=='systemProperties')].properties['java.runtime.version'].value + - os_arch=$.propertySources[?(@.name=='systemProperties')].properties['os.arch'].value + # The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^base_path^_^/env + # http request method GET POST PUT DELETE PATCH + method: GET + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-api availability monitoring + parseType: jsonPath + # http response data parse script + parseScript: '$' + # metrics - thread_state + - name: thread_state + i18n: + zh-CN: 线程状态 + en-US: Thread State + ja-JP: スレッド状態 + # Whether to display the metric group in the query result, the default is true + visible: false + # The specific monitoring metrics in the metric group. + priority: 2 + fields: + - field: state + type: 1 + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + protocol: http + http: + # Host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + # url request interface path + url: ^_^base_path^_^/metrics/jvm.threads.states + # request method GET POST PUT DELETE PATCH + method: GET + # Whether to enable ssl/tls, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + # Response data parsing method: default-system rules, jsonPath-jsonPath script, website-api availability monitoring + parseType: jsonPath + parseScript: '$.availableTags[?(@.tag == "state")].values[*]' + + - name: threads + i18n: + zh-CN: 线程 + en-US: Threads + ja-JP: スレッド情報 + priority: 3 + fields: + - field: state + type: 1 + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + label: true + - field: Size + type: 0 + i18n: + zh-CN: 数量 + en-US: Size + ja-JP: 数量 + aliasFields: + - $.measurements[?(@.statistic == "VALUE")].value + calculates: + - state='^o^state^o^' + - Size=$.measurements[?(@.statistic == "VALUE")].value + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/metrics/jvm.threads.states?tag=state:^o^state^o^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$' + + - name: space_name + i18n: + zh-CN: 空间名称 + en-US: Space Name + ja-JP: スペース名 + visible: false + priority: 4 + fields: + - field: id + type: 1 + i18n: + zh-CN: ID + en-US: ID + ja-JP: ID + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/metrics/jvm.memory.used + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.availableTags[?(@.tag == "id")].values[*]' + + - name: memory_used + i18n: + zh-CN: 内存使用 + en-US: Memory Used + ja-JP: メモリ情報 + priority: 5 + fields: + - field: space + type: 1 + i18n: + zh-CN: 空间 + en-US: Space + ja-JP: スペース + label: true + - field: mem_used + type: 0 + i18n: + zh-CN: 内存使用 + en-US: Memory Used + ja-JP: 使用したメモリ + unit: MB + aliasFields: + - $.measurements[?(@.statistic == "VALUE")].value + calculates: + - space="^o^id^o^" + - mem_used=$.measurements[?(@.statistic == "VALUE")].value + units: + - mem_used=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/metrics/jvm.memory.used?tag=id:^o^id^o^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$' + + - name: health + i18n: + zh-CN: 健康 + en-US: Health + ja-JP: 健全情報 + priority: 6 + fields: + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: 状態 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/health + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + successCodes: [200, 503] + parseType: default diff --git a/hertzbeat-manager/src/main/resources/define/app-springboot3.yml b/hertzbeat-manager/src/main/resources/define/app-springboot3.yml new file mode 100644 index 0000000..f46b569 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-springboot3.yml @@ -0,0 +1,320 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: program +# The monitoring type eg: springboot2 linux windows tomcat mysql aws... +app: springboot3 + +# The monitoring i18n name +name: + zh-CN: SpringBoot3.0 + en-US: SpringBoot3.0 + ja-JP: SpringBoot3.0 +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 <a class='help_module_content' href='http://www.tutorialspoint.com/spring_boot/spring_boot_actuator.htm'> SpringBoot3.0 Actuator </a> 暴露的通用性能指标(health、environment、threads、memory_used)进行采集监控。<span class='help_module_span'>注意⚠️:如果要监控 SpringBoot 中的信息,需要您的 SpringBoot 应用集成并开启 SpringBoot Actuator, <a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/springboot3'>点击查看具体步骤</a>。</span> + en-US: HertzBeat collect and monitors SpringBoot through general performance metric that exposed by the SpringBoot3.0 Actuator. <br><span class='help_module_span'><br>Note⚠️:You should make sure that your SpringBoot application have already integrated and enabled the SpringBoot Actuator, <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/springboot3'>click here to see the specific steps.</a></span> + zh-TW: HertzBeat 對<a class='help_module_content' href='http://www.tutorialspoint.com/spring_boot/spring_boot_actuator.htm'> SpringBoot3.0 Actuator </a>暴露的通用性能指標(health、environment、threads、memory_used)進行採集監控。<span class='help_module_span'>注意⚠️:如果要監控SpringBoot中的指標,需要您的SpringBoot應用集成並開啟SpringBoot Actuator,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/springboot3'>點擊查看具體步驟</a>。</span> + ja-JP: HertzBeat は <a class='help_module_content' href='http://www.tutorialspoint.com/spring_boot/spring_boot_actuator.htm'> SpringBoot3.0 Actuator </a> が公開する共通のパフォーマンス指標(health、environment、threads、memory_used)を収集および監視します。<span class='help_module_span'>注意⚠️:SpringBoot の情報を監視するには、SpringBoot アプリケーションに SpringBoot Actuator を統合し、有効化する必要があります。詳しい手順は <a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/springboot3'>こちらをクリック</a> してください。</span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/springboot3 + en-US: https://hertzbeat.apache.org/docs/help/springboot3 + ja-JP: https://hertzbeat.apache.org/docs/help/springboot3 +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 8080 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL利用 + # When the type is boolean, the frontend will display a switch for it. + type: boolean + # required-true or false + required: false + # field-param field key + - field: base_path + # name-param field display i18n name + name: + zh-CN: Base Path + en-US: Base Path + ja-JP: Base Path + # type-param field type(most mapping the html input type) The type "text" belongs to a text input field. + type: text + # default value + defaultValue: /actuator + # required-true or false + required: true + # hide-true or false + hide: true + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: Basic Auth Username + en-US: Basic Auth Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: Basic Auth Password + en-US: Basic Auth Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - available + - name: available + i18n: + zh-CN: 可用性 + en-US: available + ja-JP: 可用性 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), instance-is instance primary key, unit-metric unit + - field: responseTime + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + type: 0 + unit: ms + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # Specific collection configuration when protocol is http protocol + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^base_path^_^ + # http request method GET POST PUT DELETE PATCH + method: GET + # enable SSL/TLS, that is, whether it is http or https, the default is false + ssl: ^_^ssl^_^ + # http auth + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-api availability monitoring + parseType: default + + + - name: thread_state + visible: false + priority: 1 + fields: + - field: state + i18n: + zh-CN: 状态 + en-US: state + ja-JP: スレッド状態 + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/metrics/jvm.threads.states + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.availableTags[?(@.tag == "state")].values[*]' + + - name: threads + i18n: + zh-CN: 线程 + en-US: threads + ja-JP: スレッド + priority: 2 + fields: + - field: state + i18n: + zh-CN: 状态 + en-US: state + ja-JP: スレッド状態 + type: 1 + label: true + - field: Size + i18n: + zh-CN: 大小 + en-US: Size + ja-JP: サイズ + type: 0 + aliasFields: + - $.measurements[?(@.statistic == "VALUE")].value + calculates: + - state='^o^state^o^' + - Size=$.measurements[?(@.statistic == "VALUE")].value + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/metrics/jvm.threads.states?tag=state:^o^state^o^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$' + + - name: space_name + i18n: + zh-CN: 空间名称 + en-US: space name + ja-JP: スペース名 + visible: false + priority: 3 + fields: + - field: id + i18n: + zh-CN: ID + en-US: ID + ja-JP: ID + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/metrics/jvm.memory.used + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.availableTags[?(@.tag == "id")].values[*]' + + - name: memory_used + i18n: + zh-CN: 内存使用 + en-US: memory used + ja-JP: 使用したメモリ + priority: 4 + fields: + - field: space + i18n: + zh-CN: 空间 + en-US: space + ja-JP: メモリ領域 + type: 1 + label: true + - field: mem_used + i18n: + zh-CN: 已使用 + en-US: memory used + ja-JP: メモリ使用量 + type: 0 + unit: MB + aliasFields: + - $.measurements[?(@.statistic == "VALUE")].value + calculates: + - space="^o^id^o^" + - mem_used=$.measurements[?(@.statistic == "VALUE")].value + units: + - mem_used=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/metrics/jvm.memory.used?tag=id:^o^id^o^ + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$' + + - name: health + i18n: + zh-CN: 健康状态 + en-US: health + ja-JP: ヘルス情報 + priority: 5 + fields: + - field: status + i18n: + zh-CN: 状态 + en-US: status + ja-JP: ステータス + type: 1 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^base_path^_^/health + method: GET + ssl: ^_^ssl^_^ + authorization: + type: Basic Auth + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + parseType: default diff --git a/hertzbeat-manager/src/main/resources/define/app-sqlserver.yml b/hertzbeat-manager/src/main/resources/define/app-sqlserver.yml new file mode 100644 index 0000000..9c6ec1e --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-sqlserver.yml @@ -0,0 +1,357 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: sqlserver +# The monitoring i18n name +name: + zh-CN: SqlServer数据库 + en-US: SqlServer DB + ja-JP: SqlServer データベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 SqlServer 数据库的通用性能指标(系统信息、性能状态、连接等)进行采集监控,支持版本为 SqlServer 2017+。<br><a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/sqlserver'>点击查看可能遇见的问题</a>。您可以点击“<i>新建 SqlServer 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC Protocol</a> to configure SQL for collecting general metrics of SqlServer database (basic、performance counters、connection etc.). Supported version is SqlServer 2017+. <br><a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/sqlserver'>Click here to see the possible SSH problems.</a>. You can click "<i>New SqlServer Database</i>" and configure it, or select "<i>More Operations</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 協議</a> 通過配置 SQL 對 SqlServer 數據庫的通用性能指標(系統信息、性能狀態、连接等)進行采集監控,支持版本爲 SqlServer 2017+。<br><a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/sqlserver'>點擊查看可能遇見的問題</a>。您可以點擊“<i>新建 SqlServer 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBCプロトコルを介して</a> SqlServer データベース(SqlServer 2017+)の一般的なパフォーマンスのメトリクスを監視します。<br><a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/sqlserver'>可能性のある問題を見るにはここをクリックしてください。</a>。「<i>新規 SqlServer データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/sqlserver + en-US: https://hertzbeat.apache.org/docs/help/sqlserver +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 1433 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 + # field-param field key + - field: database + # name-param field display i18n name + name: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + # type-param field type(most mapping the html input tag) + type: text + # required-true or false + required: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input tag) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: url + # type-param field type(most mapping the html input tag) + name: + zh-CN: URL + en-US: URL + ja-JP: URL + # type-param field type(most mapping the html input tag) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 基本信息 + en-US: Basic + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: machine_name + i18n: + zh-CN: 机器名称 + en-US: Machine Name + ja-JP: マシン名 + type: 1 + label: true + - field: server_name + i18n: + zh-CN: 数据库服务器名称 + en-US: Server Name + ja-JP: サーバー名 + type: 1 + - field: version + i18n: + zh-CN: 数据库版本 + en-US: Version + ja-JP: バージョン + type: 1 + - field: edition + i18n: + zh-CN: 版本类型 + en-US: Edition + ja-JP: バージョンタイプ + type: 1 + - field: start_time + i18n: + zh-CN: 启动时间 + en-US: Start Time + ja-JP: 起動時間 + type: 1 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # database type + platform: sqlserver + # database username + username: ^_^username^_^ + # database password + password: ^_^password^_^ + # database name + database: ^_^database^_^ + # jdbc connect timeout + timeout: ^_^timeout^_^ + # sql query data type: oneRow, multiRow, columns + queryType: oneRow + # sql + sql: SELECT SERVERPROPERTY('MachineName') AS machine_name, SERVERPROPERTY('ServerName') AS server_name, SERVERPROPERTY('ProductVersion') AS version, SERVERPROPERTY('Edition') AS edition, sqlserver_start_time AS start_time FROM sys.dm_os_sys_info; + # custom jdbc url + url: ^_^url^_^ + + - name: performance_counters + i18n: + zh-CN: 性能指标 + en-US: Performance Counters + ja-JP: パフォーマンス情報 + priority: 1 + fields: + - field: database_pages + i18n: + zh-CN: 数据库页数 + en-US: Database Pages + ja-JP: データベースでのページ数 + type: 0 + - field: target_pages + i18n: + zh-CN: 目标页数 + en-US: Target Pages + ja-JP: バッファープール内のページ数 + type: 0 + - field: page_life_expectancy + i18n: + zh-CN: 页面平均寿命 + en-US: Page Life Expectancy + ja-JP: ページが存在する秒数 + type: 0 + - field: buffer_cache_hit_ratio + i18n: + zh-CN: 缓存命中率 + en-US: Buffer Cache Hit Ratio + ja-JP: キャッシュ命中率 + type: 0 + - field: checkpoint_pages_sec + i18n: + zh-CN: 每秒写入检查点页数 + en-US: Checkpoint Pages/sec + ja-JP: ディスクにフラッシュされた1秒あたりのページ数 + type: 0 + - field: page_reads_sec + i18n: + zh-CN: 每秒读取页面数 + en-US: Page Reads/sec + ja-JP: ページ読み取りが実行される1秒あたりの回数 + type: 0 + - field: page_writes_sec + i18n: + zh-CN: 每秒写入页面数 + en-US: Page Writes/sec + ja-JP: ページ書き込みが実行される1秒あたりの回数 + type: 0 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - Database pages + - Target pages + - Page life expectancy + - Buffer cache hit ratio + - Checkpoint pages/sec + - Page reads/sec + - Page writes/sec + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - database_pages=Database pages + - target_pages=Target pages + - page_life_expectancy=Page life expectancy + - buffer_cache_hit_ratio=Buffer cache hit ratio + - checkpoint_pages_sec=Checkpoint pages/sec + - page_reads_sec=Page reads/sec + - page_writes_sec=Page writes/sec + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: sqlserver + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: select counter_name, cntr_value from sys.dm_os_performance_counters where object_name = 'SQLServer:Buffer Manager'; + url: ^_^url^_^ + + - name: connection + i18n: + zh-CN: 连接情况 + en-US: Connection + ja-JP: 接続情報 + priority: 2 + fields: + - field: user_connection + i18n: + zh-CN: 连接数 + en-US: User Connection + ja-JP: 接続数 + type: 0 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: sqlserver + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: oneRow + sql: SELECT cntr_value as user_connection FROM sys.dm_os_performance_counters WHERE object_name = 'SQLServer:General Statistics' AND counter_name = 'User Connections'; + url: ^_^url^_^ + + - name: account_expiry + priority: 100 + i18n: + zh-CN: 数据库账号有效期 + en-US: Account Expiry + ja-JP: アカウント有効期限 + fields: + - field: login_name + type: 1 + label: true + i18n: + zh-CN: 登录名 + en-US: Login + ja-JP: ログイン + - field: days_left + type: 0 + i18n: + zh-CN: 剩余天数 + en-US: Days Left + ja-JP: 残り日数 + - field: password_expired + type: 1 + i18n: + zh-CN: 是否已过期 + en-US: Password Expired + ja-JP: 期限切れ + - field: is_disabled + type: 1 + i18n: + zh-CN: 是否禁用 + en-US: Is Disabled + ja-JP: 無効 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: sqlserver + username: ^_^username^_^ + password: ^_^password^_^ + database: master + timeout: ^_^timeout^_^ + queryType: multiRow + sql: | + SELECT name AS login_name, + ISNULL(LOGINPROPERTY(name, 'DaysUntilExpiration'), -1) AS days_left, + CASE LOGINPROPERTY(name, 'IsExpired') + WHEN 1 THEN 'Y' ELSE 'N' + END AS password_expired, + CASE is_disabled + WHEN 1 THEN 'Y' ELSE 'N' + END AS is_disabled + FROM sys.sql_logins + WHERE type_desc = 'SQL_LOGIN'; + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-ssl_cert.yml b/hertzbeat-manager/src/main/resources/define/app-ssl_cert.yml new file mode 100644 index 0000000..7eaa21b --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-ssl_cert.yml @@ -0,0 +1,168 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: ssl_cert +# The monitoring i18n name +name: + zh-CN: SSL证书 + en-US: SSL Certificate + ja-JP: SSL証明書 +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 通过证书名称、是否过期、有效期开始时间、有效期开始时间戳(ms)、过期时间、过期时间戳(ms)等指标对网站的 SSL 证书进行监测。您可以点击“<i>新建 SSL 证书</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat monitors website's SSL certificates through metrics such as certificate name, expiration date, validity start time, validity start timestamp (ms), expiration time, and expiration timestamp (ms). <br>You could click the "<i>New SSL</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: HertzBeat 通過證書名稱、是否過期、有效期開始時間、有效期開始時間戳(ms)、過期時間、過期時間戳(ms)等指標對網站的 SSL 證書進行監測。您可以點擊“<i>新建 SSL 證書</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は SSL証明書の一般的なメトリクスを監視します。<br>「<i>新規 SSL証明書</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/ssl_cert + en-US: https://hertzbeat.apache.org/docs/help/ssl_cert +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 443 + # field-param field key + - field: verify + # name-param field display i18n name + name: + zh-CN: 校验证书 + en-US: verify + ja-JP: 検証 + # When the type is boolean, the frontend will display a switch for it. + type: boolean + defaultValue: true + # required-true or false + required: false + # field-param field key + - field: uri + # name-param field display i18n name + name: + zh-CN: 相对路径 + en-US: URI + ja-JP: URI + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 200 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Website uri path(no ip port) EG:/console' + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - certificate + - name: certificate + i18n: + zh-CN: 证书 + en-US: Certificate + ja-JP: 証明書 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: subject + type: 1 + label: true + i18n: + zh-CN: 颁发对象 + en-US: Subject + ja-JP: 発行対象 + - field: expired + type: 1 + i18n: + zh-CN: 是否过期 + en-US: Expired + ja-JP: 期限切れ + - field: start_time + type: 1 + i18n: + zh-CN: 颁发日期 + en-US: Start Time + ja-JP: 発行日 + - field: start_timestamp + type: 0 + unit: ms + i18n: + zh-CN: 颁发时间戳 + en-US: Start Timestamp + ja-JP: 発行タイムスタンプ + - field: end_time + type: 1 + i18n: + zh-CN: 截止日期 + en-US: End Time + ja-JP: 期限切れ日 + - field: end_timestamp + type: 0 + unit: ms + i18n: + zh-CN: 截止时间戳 + en-US: End Timestamp + ja-JP: 期限切れタイムスタンプ + - field: days_remaining + type: 0 + unit: d + i18n: + zh-CN: 剩余天数 + en-US: Remaining Days + ja-JP: 残り日数 + aliasFields: + - subject + - expired + - start_time + - start_timestamp + - end_time + - start_timestamp + - end_timestamp + - days_remaining + calculates: + - days_remaining=(end_timestamp-now())/86400000 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssl_cert + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + ssl: ^_^verify^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-starrocks_be.yml b/hertzbeat-manager/src/main/resources/define/app-starrocks_be.yml new file mode 100644 index 0000000..8214db2 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-starrocks_be.yml @@ -0,0 +1,350 @@ +# 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. + +category: bigdata +app: starrocks_be +name: + zh-CN: StarRocks BE + en-US: StarRocks BE + ja-JP: StarRocks BE +help: + zh-CN: Hertzbeat 对 StarRocks 数据库BE的核心监控指标(CPU使用率、磁盘状态、Compaction分数、请求数、磁盘IO利用率、磁盘容量、内存使用、网络流量等)进行测量监控。<br>您可以点击 "<i>新建 StarRocks BE</i>" 并进行配置,或者选择"<i>更多操作</i>",导入已有配置。 + en-US: Hertzbeat monitoring StarRocks BE through core performance metrics such as CPU usage, disk state, compaction score, request count, disk IO utilization, disk capacity, memory usage and network traffic. You could click the "<i>New StarRocks BE Monitor</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 StarRocks 資料庫BE的核心監控指標(CPU使用率、磁碟狀態、Compaction分數、請求數、磁碟IO利用率、磁碟容量、記憶體使用、網路流量等)進行量測監控。<br>您可以點擊"<i>新建 StarRocks BE</i>"並進行配寘,或者選擇"<i>更多操作</i>",導入已有配寘。 + ja-JP: Hertzbeat は StarRocks BEの一般的なメトリクスを監視します。<br>「<i>新規 StarRocks BE</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/starrocks_be/ + en-US: https://hertzbeat.apache.org/docs/help/starrocks_be/ +params: + - field: host + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + defaultValue: 8040 + required: true + - field: timeout + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + type: number + required: false + hide: true + defaultValue: 6000 + +metrics: + - name: starrocks_be_cpu + i18n: + zh-CN: /proc/stat中的CPU使用信息 + en-US: CPU Usage in /proc/stat + ja-JP: /proc/stat内のCPU使用率 + priority: 0 + fields: + - field: mode + type: 1 + label: true + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_disks_state + i18n: + zh-CN: 磁盘状态 + en-US: Disk State + ja-JP: ディスク状態 + priority: 1 + fields: + - field: path + type: 1 + label: true + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_tablet_base_max_compaction_score + i18n: + zh-CN: Base Compaction分数 + en-US: Base Compaction Score + ja-JP: ベースの圧縮スコア + priority: 2 + fields: + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_tablet_cumulative_max_compaction_score + i18n: + zh-CN: Cumulative Compaction分数 + en-US: Cumulative Compaction Score + ja-JP: 累計圧縮スコア + priority: 3 + fields: + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_engine_requests_total + i18n: + zh-CN: 总请求数 + en-US: Total Requests + ja-JP: リクエスト総数 + priority: 4 + fields: + - field: status + type: 1 + label: true + - field: type + type: 1 + label: true + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_max_disk_io_util_percent + i18n: + zh-CN: 最大磁盘IO利用率(%) + en-US: Max Disk IO Utilization Rate(%) + ja-JP: ディスクIOの最大使用率 + priority: 5 + fields: + - field: value + type: 0 + unit: '%' + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_disks_avail_capacity + i18n: + zh-CN: 磁盘可用容量 + en-US: Disk Available Capacity + ja-JP: 利用可能なディスク容量 + priority: 6 + fields: + - field: path + type: 1 + label: true + - field: value + type: 0 + unit: MB + units: + - value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_disks_data_used_capacity + i18n: + zh-CN: 磁盘已用容量 + en-US: Disk Used Capacity + ja-JP: 使用したディスク容量 + priority: 7 + fields: + - field: path + type: 1 + label: true + - field: value + type: 0 + unit: MB + units: + - value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_load_bytes + i18n: + zh-CN: 总导入字节数 + en-US: Total Load Bytes + ja-JP: ロードバイト総数 + priority: 8 + fields: + - field: value + type: 0 + unit: bytes + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_load_rows + i18n: + zh-CN: 导入行数 + en-US: Load Rows + ja-JP: ロード行数 + priority: 9 + fields: + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_process_mem_bytes + i18n: + zh-CN: 进程内存使用 + en-US: Process Memory Usage + ja-JP: プロセスメモリ使用率 + priority: 10 + fields: + - field: value + type: 0 + unit: MB + label: true + units: + - value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_jemalloc_allocated_bytes + i18n: + zh-CN: 已分配的内存 + en-US: Allocated Memory + ja-JP: 割り当てられたメモリ + priority: 11 + fields: + - field: value + type: 0 + unit: MB + label: true + units: + - value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_network_receive_bytes + i18n: + zh-CN: 网络接收字节数 + en-US: Network Received Bytes + ja-JP: ネットワークの受信されたバイト + priority: 12 + fields: + - field: device + type: 1 + label: true + - field: value + type: 0 + unit: bytes + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_be_network_send_bytes + i18n: + zh-CN: 网络发送字节数 + en-US: Network Sent Bytes + ja-JP: ネットワークの転送されたバイト + priority: 13 + fields: + - field: device + type: 1 + label: true + - field: value + type: 0 + unit: bytes + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus diff --git a/hertzbeat-manager/src/main/resources/define/app-starrocks_fe.yml b/hertzbeat-manager/src/main/resources/define/app-starrocks_fe.yml new file mode 100644 index 0000000..999b0b5 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-starrocks_fe.yml @@ -0,0 +1,430 @@ +# 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. + +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: starrocks_fe +# The monitoring i18n name +name: + zh-CN: StarRocks FE + en-US: StarRocks FE + ja-JP: StarRocks FE +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 StarRocks 数据库FE的核心监控指标(JVM内存、线程、查询响应时间、编辑日志、加载任务、作业状态、事务状态、连接数等)进行测量监控。<br>您可以点击 "<i>新建 StarRocks FE</i>" 并进行配置,或者选择"<i>更多操作</i>",导入已有配置。 + en-US: Hertzbeat monitoring StarRocks FE through core performance metrics such as JVM memory, threads, query response time, edit logs, load jobs, job status, transaction status and connections. You could click the "<i>New StarRocks FE Monitor</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 StarRocks 資料庫FE的核心監控指標(JVM記憶體、執行緒、查詢響應時間、編輯日誌、載入任務、作業狀態、事務狀態、連接數等)進行量測監控。<br>您可以點擊"<i>新建 StarRocks FE</i>"並進行配寘,或者選擇"<i>更多操作</i>",導入已有配寘。 + ja-JP: Hertzbeat は StarRocks FEの一般的なメトリクスを監視します。<br>「<i>新規 StarRocks FE</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/starrocks_fe/ + en-US: https://hertzbeat.apache.org/docs/help/starrocks_fe/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标 Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # default value + defaultValue: 8030 + required: true + - field: timeout + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + type: number + required: false + # hide param-true or false + hide: true + defaultValue: 6000 +metrics: + - name: jvm_heap_size_bytes + i18n: + zh-CN: JVM堆内存 + en-US: JVM Heap Size + ja-JP: Java仮想マシンのヒープサイズ + priority: 0 + fields: + - field: type + type: 1 + label: true + - field: value + type: 0 + unit: MB + units: + - value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: jvm_non_heap_size_bytes + i18n: + zh-CN: JVM非堆内存 + en-US: JVM Non-Heap Size + ja-JP: Java仮想マシンのノンヒープサイズ + priority: 1 + fields: + - field: type + type: 1 + label: true + - field: value + type: 0 + unit: MB + units: + - value=B->MB + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: jvm_thread + i18n: + zh-CN: JVM线程 + en-US: JVM Thread + ja-JP: Java仮想マシンのスレッド + priority: 2 + fields: + - field: type + type: 1 + label: true + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_query_err + i18n: + zh-CN: 查询失败数 + en-US: Failed Queries + ja-JP: クエリの失敗数 + priority: 3 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_query_latency_ms + i18n: + zh-CN: 查询响应时间(ms) + en-US: Query Response Time(ms) + ja-JP: クエリの応答時間(ms) + priority: 4 + fields: + - field: quantile + type: 1 + label: true + - field: value + type: 0 + unit: ms + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_edit_log_write + i18n: + zh-CN: 编辑日志写入速率 + en-US: Edit Log Write Rate + ja-JP: 編集ログの書き込み率 + priority: 5 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_load_add + i18n: + zh-CN: 新增加载任务数 + en-US: New Load Jobs + ja-JP: ロードタスクの新規数 + priority: 6 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_load_finished + i18n: + zh-CN: 完成加载任务数 + en-US: Finished Load Jobs + ja-JP: ロードタスクの完了数 + priority: 7 + fields: + - field: value + type: 0 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_job + i18n: + zh-CN: FE作业状态 + en-US: FE Job Status + ja-JP: FEタスク状態 + priority: 8 + fields: + - field: job + type: 1 + label: true + - field: type + type: 1 + label: true + - field: state + type: 1 + label: true + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_tablet_max_compaction_score + i18n: + zh-CN: 节点上最高的 Compaction Score + en-US: Max Compaction Score on Node + ja-JP: ノードの圧縮スコアの最大値 + priority: 9 + fields: + - field: backend + type: 1 + label: true + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_meta_log_count + i18n: + zh-CN: 未做 Checkpoint 的 Edit Log 数量 + en-US: Edit Log Count without Checkpoint + ja-JP: チェックポイント無しの編集ログ数 + priority: 10 + fields: + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_query_total + i18n: + zh-CN: 查询总数 + en-US: Total Queries + ja-JP: クエリ総数 + priority: 11 + fields: + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_request_total + i18n: + zh-CN: 请求总数 + en-US: Total Requests + ja-JP: リクエスト総数 + priority: 12 + fields: + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_txn_reject + i18n: + zh-CN: 事务拒绝数 + en-US: Rejected Transactions + ja-JP: 拒否されたトランザクション数 + priority: 13 + fields: + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_txn_begin + i18n: + zh-CN: 事务开始数 + en-US: Beginning Transactions + ja-JP: 開かれたトランザクション数 + priority: 14 + fields: + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_txn_success + i18n: + zh-CN: 事务成功数 + en-US: Successful Transactions + ja-JP: 成功したトランザクション数 + priority: 15 + fields: + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_txn_failed + i18n: + zh-CN: 事务失败数 + en-US: Failed Transactions + ja-JP: 失敗したトランザクション数 + priority: 16 + fields: + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus + + - name: starrocks_fe_connection_total + i18n: + zh-CN: 连接总数 + en-US: Total Connections + ja-JP: 接続総数 + priority: 17 + fields: + - field: user + type: 1 + label: true + - field: value + type: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: '/metrics' + timeout: ^_^timeout^_^ + method: GET + parseType: prometheus \ No newline at end of file diff --git a/hertzbeat-manager/src/main/resources/define/app-storm.yml b/hertzbeat-manager/src/main/resources/define/app-storm.yml new file mode 100644 index 0000000..0bb4fc9 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-storm.yml @@ -0,0 +1,191 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: storm +# The monitoring i18n name +name: + zh-CN: Apache Storm + en-US: Apache Storm + ja-JP: Apache Storm +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Apache Storm的通用指标进行测量监控。<br>您可以点击 “<i>新建Apache Storm</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitoring Apache Storm through general performance metric. You could click the "<i>New Apache Storm</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 對 Apache Storm的通用名額進行量測監控。<br>您可以點擊“<i>新建Apache Storm</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は Apache Stormの一般的なメトリクスを監視します。<br>「<i>新規 Apache Storm</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/storm + en-US: https://hertzbeat.apache.org/docs/help/storm +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 8080 + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL + # type-param field type(boolean mapping the html switch tag) + type: boolean + # required-true or false + required: false +# collect metrics config list +metrics: + # metrics - clusterSummary + - name: clusterSummary + i18n: + zh-CN: 集群概要 + en-US: Cluster Summary + ja-JP: クラスター概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: stormVersion + type: 1 + i18n: + zh-CN: Storm 版本 + en-US: Storm Version + ja-JP: Stormバージョン + - field: supervisors + type: 0 + i18n: + zh-CN: 监督节点运行数量 + en-US: Number of supervisors running + ja-JP: 実行中の監視ノード数 + - field: topologies + type: 0 + i18n: + zh-CN: 拓扑运行数量 + en-US: Number of topologies running + ja-JP: 実行中のトポロジーノード数 + - field: slotsTotal + type: 0 + i18n: + zh-CN: 工作槽总数 + en-US: Total number of available worker slots + ja-JP: 使用可能なワーカースロット総数 + - field: slotsUsed + type: 0 + i18n: + zh-CN: 已用工作槽数量 + en-US: Number of worker slots used + ja-JP: 使用したワーカースロット数 + - field: slotsFree + type: 0 + i18n: + zh-CN: 可用工作槽数量 + en-US: Number of worker slots available + ja-JP: 使用可能なワーカースロット数 + - field: executorsTotal + type: 0 + i18n: + zh-CN: 执行者数量 + en-US: Total number of executors + ja-JP: 執行者総数 + - field: tasksTotal + type: 0 + i18n: + zh-CN: 任务总数 + en-US: Total tasks + ja-JP: タスク総数 + - field: totalMem + type: 0 + unit: MB + i18n: + zh-CN: 集群中的内存总量(以 MB 为单位) + en-US: The total amount of memory in the cluster in MB + ja-JP: クラスター内のメモリ容量(MB) + - field: totalCpu + type: 0 + i18n: + zh-CN: 集群中的CPU总量 + en-US: The total amount of CPU in the cluster + ja-JP: クラスター内のCPU総数 + - field: availMem + type: 0 + unit: MB + i18n: + zh-CN: 集群中的可用内存量(以 MB 为单位) + en-US: The amount of available memory in the cluster in MB + ja-JP: クラスター内の使用可能なメモリ(MB) + - field: availCpu + type: 0 + i18n: + zh-CN: 集群中的可用CPU数量 + en-US: The amount of available cpu in the cluster + ja-JP: クラスター内の使用可能なCPU数 + - field: memAssignedPercentUtil + type: 0 + unit: '%' + i18n: + zh-CN: 集群中分配的内存资源利用率百分比 + en-US: The percent utilization of assigned memory resources in cluster + ja-JP: クラスター内の割り当てメモリリソースの比率 + - field: cpuAssignedPercentUtil + type: 0 + unit: '%' + i18n: + zh-CN: 集群中分配的CPU资源利用率百分比 + en-US: The percent utilization of assigned CPU resources in cluster + ja-JP: クラスター内の割り当てCPUリソースの比率 + + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: /api/v1/cluster/summary + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: default diff --git a/hertzbeat-manager/src/main/resources/define/app-synology_nas.yml b/hertzbeat-manager/src/main/resources/define/app-synology_nas.yml new file mode 100644 index 0000000..16c3146 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-synology_nas.yml @@ -0,0 +1,863 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: server +# The monitoring type eg: linux windows tomcat mysql aws... +app: synology_nas +# The monitoring i18n name +name: + zh-CN: Synology NAS + en-US: Synology NAS + ja-JP: Synology NAS +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 协议</a> 对 Synology NAS 的通用指标进行采集监控。<br><span class='help_module_span'>注意⚠️:Synology NAS 要开启 SNMP 服务,<a class='help_module_content' href='https://kb.synology.cn/zh-cn/DSM/help/DSM/AdminCenter/system_snmp'>点击查看开启步骤</a>。</span>您可以点击 “<i>新建 Synology NAS</i>” 并进行配置SNMP相关参数添加,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP Protocol</a> to monitoring Synology NAS general performance metrics. <br><span class='help_module_span'>Note⚠️:Synology NAS need enable the SNMP services <a class='help_module_content' href='https://kb.synology.com/en-global/DSM/help/DSM/AdminCenter/system_snmp'>Click here to view the specific steps.</a></span> You can click the "<i>Synology NAS</i>" button and config snmp params to add monitor or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 协议</a> 对 Synology NAS 的通用指标进行采集监控。<br><span class='help_module_span'>注意⚠️:Synology NAS 需要开启 SNMP 服务,<a class='help_module_content' href='https://kb.synology.com/zh-tw/DSM/help/DSM/AdminCenter/system_snmp'>点击查看开启步骤</a>。</span>您可以点击 “<i>新建 Synology NAS</i>” 并进行配置SNMP相关参数添加,或者选择“<i>更多操作</i>”,导入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP プロトコルを介して</a> Synology NAS 一般的なメトリクスを監視します。<br><span class='help_module_span'>注意⚠️:Synology NAS で SNMP サービスを有効にする必要があります,<a class='help_module_content' href='https://kb.synology.com/ja-jp/DSM/help/DSM/AdminCenter/system_snmp'>クリックしてガイドを見ます</a>。</span><br>「<i>新規 Synology NAS</i>」をクリックしてSNMPなどのパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/synology_nas + en-US: https://hertzbeat.apache.org/docs/help/synology_nas +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 161 + # field-param field key + - field: snmpVersion + # name-param field display i18n name + name: + zh-CN: SNMP 版本 + en-US: SNMP Version + ja-JP: SNMPバージョン + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: SNMPv1 + value: 0 + - label: SNMPv2c + value: 1 + - label: SNMPv3 + value: 3 + # field-param field key + - field: community + # name-param field display i18n name + name: + zh-CN: SNMP 团体字 + en-US: SNMP Community + ja-JP: SNMPコミュニティ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp community for v1 v2c' + # dependent parameter values list + depend: + snmpVersion: + - 0 + - 1 + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: SNMP username + en-US: SNMP username + ja-JP: SNMPユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp username for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: contextName + # name-param field display i18n name + name: + zh-CN: SNMP contextName + en-US: SNMP contextName + ja-JP: SNMPコンテキスト名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp contextName for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP authPassword + en-US: SNMP authPassword + ja-JP: SNMP認証パスワード + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPasswordEncryption + # name-param field display i18n name + name: + zh-CN: authPassword 加密方式 + en-US: authPassword Encryption + ja-JP: 認証暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: MD5 + value: 0 + - label: SHA1 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP privPassphrase + en-US: SNMP privPassphrase + ja-JP: SNMPパスワードフレーズ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPasswordEncryption + # name-param field display i18n name + name: + zh-CN: privPassword 加密方式 + en-US: privPassword Encryption + ja-JP: パスワードの暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: DES + value: 0 + - label: AES128 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: false + # hide-is hide this field and put it in advanced layout + hide: true + # default value + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - system + - name: system + i18n: + zh-CN: 系统 + en-US: System + ja-JP: システム + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + fields: + - field: modelName + type: 1 + i18n: + zh-CN: 型号 + en-US: Name + ja-JP: モデル + - field: serialNumber + type: 1 + i18n: + zh-CN: 序列号 + en-US: Serial Number + ja-JP: シリアル番号 + - field: version + type: 1 + i18n: + zh-CN: DSM 版本 + en-US: Version + ja-JP: DSM バージョン + - field: controllerNumber + type: 1 + i18n: + zh-CN: 控制器编号 + en-US: Number of Users + ja-JP: コントローラー番号 + protocol: snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: walk + oids: + modelName: 1.3.6.1.4.1.6574.1.5.1 + serialNumber: 1.3.6.1.4.1.6574.1.5.2 + version: 1.3.6.1.4.1.6574.1.5.3 + controllerNumber: 1.3.6.1.4.1.6574.1.6 + + - name: status + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + priority: 1 + fields: + - field: powerStatus + type: 0 + i18n: + zh-CN: 电源 + en-US: Power + ja-JP: 電源 + - field: systemFanStatus + type: 0 + i18n: + zh-CN: 系统风扇 + en-US: System Fan + ja-JP: システムファン + - field: cpuFanStatus + type: 0 + i18n: + zh-CN: CPU 风扇 + en-US: CPU Fan + ja-JP: CPUファン + - field: systemStatus + type: 0 + i18n: + zh-CN: 系统分区 + en-US: System Partition + ja-JP: システムパーティション + - field: thermalStatus + type: 0 + i18n: + zh-CN: 散热状态 + en-US: Thermal Partition + ja-JP: 熱源 + - field: temperature + type: 0 + i18n: + zh-CN: 温度 + en-US: Temperature + ja-JP: 温度 + - field: cpuUtilization + type: 0 + i18n: + zh-CN: CPU 利用率 + en-US: CPU Utilization + ja-JP: CPU 使用率 + - field: memUtilization + type: 0 + i18n: + zh-CN: 内存利用率 + en-US: Memory Utilization + ja-JP: メモリ使用率 + protocol: snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: walk + oids: + systemStatus: 1.3.6.1.4.1.6574.1.1 + temperature: 1.3.6.1.4.1.6574.1.2 + powerStatus: 1.3.6.1.4.1.6574.1.3 + systemFanStatus: 1.3.6.1.4.1.6574.1.4.1 + cpuFanStatus: 1.3.6.1.4.1.6574.1.4.2 + cpuUtilization: 1.3.6.1.4.1.6574.1.7.1 + memUtilization: 1.3.6.1.4.1.6574.1.7.2 + thermalStatus: 1.3.6.1.4.1.6574.1.78 + + - name: disk + i18n: + zh-CN: 硬盘 + en-US: Disk + ja-JP: ディスク + priority: 2 + fields: + - field: diskID + type: 1 + i18n: + zh-CN: ID + en-US: ID + ja-JP: ディスクID + - field: diskModel + type: 1 + i18n: + zh-CN: 型号 + en-US: Model + ja-JP: ディスクモデル + - field: diskType + type: 1 + i18n: + zh-CN: 类型 + en-US: Type + ja-JP: ディスクタイプ + - field: diskStatus + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ディスクステータス + - field: diskTemperature + type: 0 + i18n: + zh-CN: 温度 + en-US: Temperature + ja-JP: ディスク温度 + - field: diskRole + type: 1 + i18n: + zh-CN: 作用 + en-US: Role + ja-JP: ディスク作用 + - field: diskRetry + type: 0 + i18n: + zh-CN: 连接重试次数 + en-US: Retry count + ja-JP: ディスクリミネーション + - field: diskBadSector + type: 0 + i18n: + zh-CN: 坏扇区数 + en-US: Bad Sector + ja-JP: ディスクバッドセクタ + - field: diskIdentifyFail + type: 0 + i18n: + zh-CN: 计数失败 + en-US: Identify Fail + ja-JP: ディスク識別失敗 + - field: diskRemainLife + type: 0 + i18n: + zh-CN: 预计剩余寿命 + en-US: Remain Life + ja-JP: ディスク残り寿命 + - field: diskName + type: 1 + i18n: + zh-CN: 保留名称 + en-US: Disk Name + ja-JP: ディスク名 + - field: diskHealthStatus + type: 0 + i18n: + zh-CN: 健康状况 + en-US: Health Status + ja-JP: ディスクヘルスステータス + protocol: snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: walk + oids: + diskID: 1.3.6.1.4.1.6574.2.1.1.2 + diskModel: 1.3.6.1.4.1.6574.2.1.1.3 + diskType: 1.3.6.1.4.1.6574.2.1.1.4 + diskStatus: 1.3.6.1.4.1.6574.2.1.1.5 + diskTemperature: 1.3.6.1.4.1.6574.2.1.1.6 + diskRole: 1.3.6.1.4.1.6574.2.1.1.7 + diskRetry: 1.3.6.1.4.1.6574.2.1.1.8 + diskBadSector: 1.3.6.1.4.1.6574.2.1.1.9 + diskIdentifyFail: 1.3.6.1.4.1.6574.2.1.1.10 + diskRemainLife: 1.3.6.1.4.1.6574.2.1.1.11 + diskName: 1.3.6.1.4.1.6574.2.1.1.12 + diskHealthStatus: 1.3.6.1.4.1.6574.2.1.1.13 + + - name: raid + i18n: + zh-CN: RAID + en-US: RAID + ja-JP: RAID + priority: 3 + fields: + - field: raidName + type: 1 + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名 + - field: raidStatus + type: 0 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ディスクモデル + - field: raidFreeSize + type: 0 + i18n: + zh-CN: 可用容量 + en-US: Free Size + ja-JP: ディスク残りサイズ + - field: raidTotalSize + type: 0 + i18n: + zh-CN: 总大小 + en-US: Total Size + ja-JP: ディスクサイズ + - field: raidHotspareCnt + type: 0 + i18n: + zh-CN: 热备数 + en-US: Hot Spare Count + ja-JP: ディスクホットスパア数 + protocol: snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: walk + oids: + raidName: 1.3.6.1.4.1.6574.3.1.1.2 + raidStatus: 1.3.6.1.4.1.6574.3.1.1.3 + raidFreeSize: 1.3.6.1.4.1.6574.3.1.1.4 + raidTotalSize: 1.3.6.1.4.1.6574.3.1.1.5 + raidHotspareCnt: 1.3.6.1.4.1.6574.3.1.1.6 + + - name: smart + i18n: + zh-CN: S.M.A.R.T + en-US: S.M.A.R.T + ja-JP: S.M.A.R.T + priority: 4 + fields: + - field: diskSMARTInfoDevName + type: 1 + i18n: + zh-CN: 设备名称 + en-US: Device Name + ja-JP: ディスク名 + - field: diskSMARTAttrName + type: 1 + i18n: + zh-CN: 属性名 + en-US: Attribute Name + ja-JP: ディスク属性名 + - field: diskSMARTAttrId + type: 1 + i18n: + zh-CN: 属性 ID + en-US: Attribute ID + ja-JP: ディスク属性ID + - field: diskSMARTAttrCurrent + type: 0 + i18n: + zh-CN: 当前值 + en-US: Attribute Current + ja-JP: ディスク属性現在値 + - field: diskSMARTAttrWorst + type: 0 + i18n: + zh-CN: 最差值 + en-US: Attribute Worst + ja-JP: ディスク属性最差値 + - field: diskSMARTAttrThreshold + type: 0 + i18n: + zh-CN: 阈值 + en-US: Attribute Threshold + ja-JP: ディスク属性しきい値 + - field: diskSMARTAttrRaw + type: 0 + i18n: + zh-CN: 原始值 + en-US: Raw + ja-JP: ディスク属性原始値 + - field: diskSMARTAttrStatus + type: 1 + i18n: + zh-CN: 信息状态 + en-US: Attribute Status + ja-JP: ディスク属性情報ステータス + protocol: snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: walk + oids: + diskSMARTInfoDevName: 1.3.6.1.4.1.6574.5.1.1.2 + diskSMARTAttrName: 1.3.6.1.4.1.6574.5.1.1.3 + diskSMARTAttrId: 1.3.6.1.4.1.6574.5.1.1.4 + diskSMARTAttrCurrent: 1.3.6.1.4.1.6574.5.1.1.5 + diskSMARTAttrWorst: 1.3.6.1.4.1.6574.5.1.1.6 + diskSMARTAttrThreshold: 1.3.6.1.4.1.6574.5.1.1.7 + diskSMARTAttrRaw: 1.3.6.1.4.1.6574.5.1.1.8 + diskSMARTAttrStatus: 1.3.6.1.4.1.6574.5.1.1.9 + + - name: spaceio + i18n: + zh-CN: 存储空间 I/O + en-US: Space I/O + ja-JP: スペースI/O + priority: 5 + fields: + - field: spaceIODevice + type: 1 + i18n: + zh-CN: 设备名称 + en-US: Device Name + ja-JP: ディスク名 + - field: spaceIOReads + type: 0 + i18n: + zh-CN: 读取访问次数 + en-US: Read Accesses Count + ja-JP: ディスク属性読み込みアクセス回数 + - field: spaceIOWrites + type: 0 + i18n: + zh-CN: 写入访问次数 + en-US: Write Accesses Count + ja-JP: ディスク属性書き込みアクセス回数 + - field: spaceIOLA + type: 0 + unit: '%' + i18n: + zh-CN: 硬盘负载 + en-US: The load of disk + ja-JP: ディスク属性ディスクロード + - field: spaceIOLA1 + type: 0 + unit: '%' + i18n: + zh-CN: 1 分钟平均负载 + en-US: The 1-minute average load of disk + ja-JP: 1分平均負荷 + - field: spaceIOLA5 + type: 0 + unit: '%' + i18n: + zh-CN: 5分钟平均负载 + en-US: The 5-minute average load of disk + ja-JP: 5分平均負荷 + - field: spaceIOLA15 + type: 0 + unit: '%' + i18n: + zh-CN: 15 分钟平均负载 + en-US: The-15 minute average load of disk + ja-JP: 15分平均負荷 + - field: spaceUUID + type: 1 + i18n: + zh-CN: 卷UUID + en-US: UUID + ja-JP: UUID + protocol: snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: walk + oids: + spaceIODevice: 1.3.6.1.4.1.6574.102.1.1.2 + spaceIOReads: 1.3.6.1.4.1.6574.102.1.1.5 + spaceIOWrites: 1.3.6.1.4.1.6574.102.1.1.6 + spaceIOLA: 1.3.6.1.4.1.6574.102.1.1.7 + spaceIOLA1: 1.3.6.1.4.1.6574.102.1.1.8 + spaceIOLA5: 1.3.6.1.4.1.6574.102.1.1.9 + spaceIOLA15: 1.3.6.1.4.1.6574.102.1.1.10 + spaceUUID: 1.3.6.1.4.1.6574.102.1.1.13 + + - name: storageio + i18n: + zh-CN: 硬盘 I/O + en-US: Storage I/O + ja-JP: ハードディスクI/O + priority: 6 + fields: + - field: storageIODevice + type: 1 + i18n: + zh-CN: 设备名称 + en-US: Device Name + ja-JP: ディスク名 + - field: storageIOReads + type: 0 + i18n: + zh-CN: 读取访问次数 + en-US: Read Accesses Count + ja-JP: ディスク属性読み込みアクセス回数 + - field: storageIOWrites + type: 0 + i18n: + zh-CN: 写入访问次数 + en-US: Write Accesses Count + ja-JP: ディスク属性書き込みアクセス回数 + - field: storageIOLA + type: 0 + unit: '%' + i18n: + zh-CN: 硬盘负载 + en-US: The load of disk + ja-JP: ディスク属性ディスクロード + - field: storageIOLA1 + type: 0 + unit: '%' + i18n: + zh-CN: 1 分钟平均负载 + en-US: The 1-minute average load of disk + ja-JP: 1分平均負荷 + - field: storageIOLA5 + type: 0 + unit: '%' + i18n: + zh-CN: 5分钟平均负载 + en-US: The 5-minute average load of disk + ja-JP: 5分平均負荷 + - field: storageIOLA15 + type: 0 + unit: '%' + i18n: + zh-CN: 15 分钟平均负载 + en-US: The-15 minute average load of disk + ja-JP: 15分平均負荷 + - field: storageIODeviceSerial + type: 1 + i18n: + zh-CN: 序列号 + en-US: Serial Number + ja-JP: シリアルナンバー + protocol: snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: walk + oids: + storageIODevice: 1.3.6.1.4.1.6574.101.1.1.2 + storageIOReads: 1.3.6.1.4.1.6574.101.1.1.5 + storageIOWrites: 1.3.6.1.4.1.6574.101.1.1.6 + storageIOLA: 1.3.6.1.4.1.6574.101.1.1.7 + storageIOLA1: 1.3.6.1.4.1.6574.101.1.1.8 + storageIOLA5: 1.3.6.1.4.1.6574.101.1.1.9 + storageIOLA15: 1.3.6.1.4.1.6574.101.1.1.10 + storageIODeviceSerial: 1.3.6.1.4.1.6574.101.1.1.13 diff --git a/hertzbeat-manager/src/main/resources/define/app-tdengine_promql.yml b/hertzbeat-manager/src/main/resources/define/app-tdengine_promql.yml new file mode 100644 index 0000000..67accd2 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-tdengine_promql.yml @@ -0,0 +1,1537 @@ +# 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. + +# The monitoring type category: service-application service monitoring, db-database monitoring, custom-custom monitoring. +category: custom +# The monitoring application type name (consistent with the file name) +app: tdengine_promql +name: + zh-CN: TDengine-PromQL + en-US: TDengine-PromQL + ja-JP: TDengine-PromQL +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 Prometheus PromQL 从 Prometheus 服务器中查询到 TDengine 的通用指标数据来进行监控。此方案适用于 Prometheus 已监控 TDengine,需要从 Prometheus 服务器抓取 TDengine 的监控数据。<br>您可以点击 “<i>新建 TDengine-PromQL</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat uses Prometheus PromQL to query general metric data from the Prometheus server for TDengine monitoring. This solution is suitable for Prometheus that has already monitored TDengine and needs to retrieve TDengine monitoring data from the Prometheus server. <br>You can click "<i>New TDengine-PromQL</i>" and configure it, or select "<i>More Actions</i>" to import an existing configuration. + ja-JP: Hertzbeat は Prometheus PromQL を介して Prometheus サーバーに TDengine の一般的なパフォーマンスのメトリクスをクエリして監視します。このシナリオは、PrometheusがすでにTDengineを監視しており、PrometheusサーバーからTDengineの監視データを取得する必要がある場合に適用されます。<br>「<i>新規 TDengine-PromQL</i>」をクリックして設定しましょう。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/tdengine_promql + en-US: https://hertzbeat.apache.org/docs/help/tdengine_promql +params: + - field: host + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + required: true + defaultValue: 9090 + - field: method + name: + zh-CN: 请求方式 + en-US: Method + ja-JP: リクエストメソッド + type: radio + required: true + options: + - label: GET请求 + value: GET + - label: POST请求 + value: POST + - label: PUT请求 + value: PUT + - label: DELETE请求 + value: DELETE + defaultValue: GET + - field: uri + name: + zh-CN: 相对路径 + en-US: URI + ja-JP: URI + type: text + limit: 200 + required: true + placeholder: 'API地址除IP端口外的路径 例如:/v2/book/bar' + defaultValue: /api/v1/query + - field: ssl + name: + zh-CN: 启动SSL + en-US: SSL + ja-JP: SSL + type: boolean + required: false + - field: headers + name: + zh-CN: 请求Headers + en-US: Headers + ja-JP: ヘッダ + type: key-value + required: false + keyAlias: Header Name + valueAlias: Header Value + - field: params + name: + zh-CN: 查询Params + en-US: Params + ja-JP: パラメータ + type: key-value + required: false + keyAlias: Param Key + valueAlias: Param Value + - field: contentType + name: + zh-CN: Content-Type + en-US: Content-Type + ja-JP: コンテンツタイプ + type: text + placeholder: '请求BODY资源类型' + required: false + hide: true + - field: payload + name: + zh-CN: 请求BODY + en-US: BODY + ja-JP: ボディ + type: textarea + placeholder: 'POST PUT请求时有效' + required: false + hide: true + - field: authType + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + type: radio + required: false + hide: true + options: + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + hide: true + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + hide: true + +# collect metrics config list +metrics: + # Basic information indicators + - name: taos_cluster_info_first_ep + i18n: + zh-CN: 第一个端点信息 + en-US: First Endpoint Info + ja-JP: 最初のエンドポイント情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 1 + i18n: + zh-CN: 端点值 + en-US: Endpoint Value + ja-JP: エンドポイント値 + label: true + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # Host: IPv4, IPv6 or domain name of the host. + host: ^_^host^_^ + # port + port: ^_^port^_^ + #URL request path. + url: ^_^uri^_^ + # request methods:GET POST PUT DELETE PATCH + method: ^_^method^_^ + # Whether to enable SSL/TLS, i.e. whether it is HTTP or HTTPS, default is false. + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + # Header content + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + # Request parameter content + params: + query: taos_cluster_info_first_ep + # authorization + authorization: + # Authentication method: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # Response data parsing method: default - system rules, jsonPath - jsonPath script, website - API availability monitoring. + parseType: PromQL + + - name: taos_cluster_info_version + i18n: + zh-CN: 版本信息 + en-US: Version Info + ja-JP: バージョン情報 + priority: 1 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 1 + i18n: + zh-CN: 版本号 + en-US: Version Number + ja-JP: バージョン番号 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_version + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_cluster_info_master_uptime + i18n: + zh-CN: 主节点运行时间(天) + en-US: Master Node Runtime + ja-JP: マスターノードのランタイム(日) + priority: 2 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 天 + en-US: Day + ja-JP: 日 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_master_uptime + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + # Number Of nodes indicator + - name: taos_cluster_info_dnodes_total + i18n: + zh-CN: Dnode 总数 + en-US: Total Of Dnodes + ja-JP: Dnode総数 + priority: 3 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 总数 + en-US: Total + ja-JP: 総数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_dnodes_total + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_cluster_info_dnodes_alive + i18n: + zh-CN: Dnode 存活数 + en-US: Dnode Survival Count + ja-JP: Dnodeのサバイバル数 + priority: 4 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 数量 + en-US: value + ja-JP: 数量 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_dnodes_alive + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_cluster_info_mnodes_total + i18n: + zh-CN: Mnode 总数 + en-US: Total Of Mnodes + ja-JP: Mnode総数 + priority: 5 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 总数 + en-US: Total + ja-JP: 総数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_mnodes_total + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_cluster_info_mnodes_alive + i18n: + zh-CN: Mnode 存活数 + en-US: Mnode Survival Count + ja-JP: Mnodeのサバイバル数 + priority: 6 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 数量 + en-US: value + ja-JP: 数量 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_mnodes_alive + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_cluster_info_vgroups_total + i18n: + zh-CN: VGroup总数 + en-US: Total Of Virtual Groups + ja-JP: 仮想グループの総数 + priority: 7 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 总数 + en-US: Total + ja-JP: 総数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_vgroups_total + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_cluster_info_vgroups_alive + i18n: + zh-CN: 存活的虚拟组数量 + en-US: Virtual Groups Survival Count + ja-JP: 仮想グループのサバイバル数 + priority: 8 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 数量 + en-US: value + ja-JP: 数量 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_vgroups_alive + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_cluster_info_vnodes_total + i18n: + zh-CN: VNode 总数 + en-US: Total Virtual Nodes + ja-JP: 仮想ノードの総数 + priority: 9 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 总数 + en-US: Total + ja-JP: 総数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_vnodes_total + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_cluster_info_vnodes_alive + i18n: + zh-CN: VNode 存活数 + en-US: VNodes Survival Count + ja-JP: 仮想ノードのサバイバル数 + priority: 10 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 数量 + en-US: value + ja-JP: 数量 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_vnodes_alive + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + # Database and table statistics + - name: taos_cluster_info_dbs_total + i18n: + zh-CN: 数据库总数 + en-US: Total Databases + ja-JP: データベース総数 + priority: 11 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 总数 + en-US: Total + ja-JP: 総数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_dbs_total + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_cluster_info_tbs_total + i18n: + zh-CN: 表总数 + en-US: Total Tables + ja-JP: テーブル総数 + priority: 12 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 总数 + en-US: Total + ja-JP: 総数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_tbs_total + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_cluster_info_connections_total + i18n: + zh-CN: 连接总数 + en-US: Total Connections + ja-JP: 接続総数 + priority: 13 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 总数 + en-US: Total + ja-JP: 総数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_cluster_info_connections_total + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + # Dnode information + - name: taos_d_info_status + i18n: + zh-CN: Dnode 状态 + en-US: Dnode Status + ja-JP: Dnode状態 + priority: 14 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: dnode_ep + type: 1 + i18n: + zh-CN: Dnode 端点 + en-US: Dnode Endpoint + ja-JP: Dnodeのエンドポイント + label: true + - field: dnode_id + type: 1 + i18n: + zh-CN: Dnode Id + en-US: Dnode Id + ja-JP: Dnode Id + label: true + - field: value + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: 状態 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_d_info_status + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_dnodes_info_uptime + i18n: + zh-CN: Dnode 的启动时间(秒) + en-US: Dnode uptime(seconds) + ja-JP: Dnodeのアップタイム(秒) + priority: 15 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: dnode_ep + type: 1 + i18n: + zh-CN: Dnode 端点 + en-US: Dnode Endpoint + ja-JP: Dnodeのエンドポイント + label: true + - field: dnode_id + type: 1 + i18n: + zh-CN: Dnode Id + en-US: Dnode Id + ja-JP: Dnode Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 启动时间 + en-US: uptime + ja-JP: アップタイム + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_dnodes_info_uptime + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_dnodes_info_cpu_engine + i18n: + zh-CN: Dnode 的进程所使用的 CPU 百分比 + en-US: Dnode Process CPU Usage (%) + ja-JP: DnodeプロセスのCPU使用率(%) + priority: 16 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: dnode_ep + type: 1 + i18n: + zh-CN: Dnode 端点 + en-US: Dnode Endpoint + ja-JP: Dnodeのエンドポイント + label: true + - field: dnode_id + type: 1 + i18n: + zh-CN: Dnode Id + en-US: Dnode Id + ja-JP: Dnode Id + label: true + - field: value + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + aliasFields: + - cluster_id + - dnode_ep + - dnode_id + - value + calculates: + - cluster_id=cluster_id + - dnode_ep=dnode_ep + - dnode_id=dnode_id + - value=value*100 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_dnodes_info_cpu_engine + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_dnodes_info_cpu_system + i18n: + zh-CN: Dnode 节点的系统使用的 CPU 百分比 + en-US: Dnode System CPU Usage (%) + ja-JP: DnodeシステムのCPU使用率(%) + priority: 17 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: dnode_ep + type: 1 + i18n: + zh-CN: Dnode 端点 + en-US: Dnode Endpoint + ja-JP: Dnodeのエンドポイント + label: true + - field: dnode_id + type: 1 + i18n: + zh-CN: Dnode Id + en-US: Dnode Id + ja-JP: Dnode Id + label: true + - field: value + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + aliasFields: + - cluster_id + - dnode_ep + - dnode_id + - value + calculates: + - cluster_id=cluster_id + - dnode_ep=dnode_ep + - dnode_id=dnode_id + - value=value*100 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_dnodes_info_cpu_system + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_dnodes_info_mem_engine + i18n: + zh-CN: Dnode 进程所使用的内存 + en-US: Memory Usage of Dnode Processes + ja-JP: Dnodeプロセスのメモリ使用率 + priority: 18 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: dnode_ep + type: 1 + i18n: + zh-CN: Dnode 端点 + en-US: Dnode Endpoint + ja-JP: Dnodeのエンドポイント + label: true + - field: dnode_id + type: 1 + i18n: + zh-CN: Dnode Id + en-US: Dnode Id + ja-JP: Dnode Id + label: true + - field: value + type: 0 + unit: MB + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + aliasFields: + - cluster_id + - dnode_ep + - dnode_id + - value + calculates: + - cluster_id=cluster_id + - dnode_ep=dnode_ep + - dnode_id=dnode_id + - value=value/1024 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_dnodes_info_mem_engine + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_dnodes_info_mem_system + i18n: + zh-CN: Dnode 节点的系统使用的内存 + en-US: Dnode System Memory Usage + ja-JP: Dnodeシステムのメモリ使用率 + priority: 19 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: dnode_ep + type: 1 + i18n: + zh-CN: Dnode 端点 + en-US: Dnode Endpoint + ja-JP: Dnodeのエンドポイント + label: true + - field: dnode_id + type: 1 + i18n: + zh-CN: Dnode Id + en-US: Dnode Id + ja-JP: Dnode Id + label: true + - field: value + type: 0 + unit: KB + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + aliasFields: + - cluster_id + - dnode_ep + - dnode_id + - value + calculates: + - cluster_id=cluster_id + - dnode_ep=dnode_ep + - dnode_id=dnode_id + - value=value/1024 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_dnodes_info_mem_engine + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_dnodes_info_disk_total + i18n: + zh-CN: Dnode 所在节点的磁盘总容量(单位 Byte) + en-US: Dnode Total Disk Capacity (Byte) + ja-JP: Dnodeディスク容量合計(Byte) + priority: 20 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: dnode_ep + type: 1 + i18n: + zh-CN: Dnode 端点 + en-US: Dnode Endpoint + ja-JP: Dnodeのエンドポイント + label: true + - field: dnode_id + type: 1 + i18n: + zh-CN: Dnode Id + en-US: Dnode Id + ja-JP: Dnode Id + label: true + - field: value + type: 0 + unit: Byte + i18n: + zh-CN: 总数 + en-US: total + ja-JP: 合計 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_dnodes_info_disk_total + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_dnodes_info_disk_used + i18n: + zh-CN: Dnode 所在节点的磁盘已使用的容量(单位 Byte) + en-US: Dnode Used Disk Capacity (Byte) + ja-JP: Dnodeが使用したディスク容量合計(Byte) + priority: 21 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: dnode_ep + type: 1 + i18n: + zh-CN: Dnode 端点 + en-US: Dnode Endpoint + ja-JP: Dnodeのエンドポイント + label: true + - field: dnode_id + type: 1 + i18n: + zh-CN: Dnode Id + en-US: Dnode Id + ja-JP: Dnode Id + label: true + - field: value + type: 0 + unit: Byte + i18n: + zh-CN: 总数 + en-US: total + ja-JP: 合計 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_dnodes_info_disk_used + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_dnodes_info_io_write_disk + i18n: + zh-CN: Dnode 所在节点的磁盘 io 写入速率(单位 Byte/s) + en-US: Dnode Node Disk I/O Write Rate (Byte/s) + ja-JP: DnodeのディスクIO書き込み率(Byte/s) + priority: 22 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: dnode_ep + type: 1 + i18n: + zh-CN: Dnode 端点 + en-US: Dnode Endpoint + ja-JP: Dnodeのエンドポイント + label: true + - field: dnode_id + type: 1 + i18n: + zh-CN: Dnode Id + en-US: Dnode Id + ja-JP: Dnode Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 速率 + en-US: value + ja-JP: 書き込み率 + label: true + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_dnodes_info_io_write_disk + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_dnodes_info_io_read_disk + i18n: + zh-CN: Dnode 所在节点的磁盘 io 读取速率(单位 Byte/s) + en-US: Dnode Disk I/O Read Rate (Byte/s) + ja-JP: DnodeのディスクIO読み込み率(Byte/s) + priority: 23 + fields: + - field: cluster_id + type: 1 + i18n: + zh-CN: 集群ID + en-US: Cluster Id + ja-JP: クラスター Id + label: true + - field: dnode_ep + type: 1 + i18n: + zh-CN: Dnode 端点 + en-US: Dnode Endpoint + ja-JP: Dnodeのエンドポイント + label: true + - field: dnode_id + type: 1 + i18n: + zh-CN: Dnode Id + en-US: Dnode Id + ja-JP: Dnode Id + label: true + - field: value + type: 0 + i18n: + zh-CN: 速率 + en-US: value + ja-JP: 読み込み率 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_dnodes_info_io_write_disk + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + # taosadapter Related + - name: taos_adapter_requests_total + i18n: + zh-CN: 总请求数 + en-US: Total Requests + ja-JP: リクエスト総数 + priority: 24 + fields: + - field: endpoint + type: 1 + i18n: + zh-CN: 端点 + en-US: Endpoint + ja-JP: エンドポイント + label: true + - field: req_type + type: 1 + i18n: + zh-CN: 请求类型 + en-US: Req Type + ja-JP: リクエストタイプ + label: true + - field: value + type: 0 + i18n: + zh-CN: 总数 + en-US: Total + ja-JP: 総数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_adapter_requests_total + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_adapter_requests_success + i18n: + zh-CN: 成功的请求数 + en-US: Successful Requests + ja-JP: 成功したリクエスト総数 + priority: 25 + fields: + - field: endpoint + type: 1 + i18n: + zh-CN: 端点 + en-US: Endpoint + ja-JP: エンドポイント + label: true + - field: req_type + type: 1 + i18n: + zh-CN: 请求类型 + en-US: Req Type + ja-JP: リクエストタイプ + label: true + - field: value + type: 0 + i18n: + zh-CN: 总数 + en-US: Total + ja-JP: 総数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_adapter_requests_success + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_adapter_requests_fail + i18n: + zh-CN: 失败的请求数 + en-US: Failed Requests + ja-JP: 失敗したリクエスト総数 + priority: 26 + fields: + - field: endpoint + type: 1 + i18n: + zh-CN: 端点 + en-US: Endpoint + ja-JP: エンドポイント + label: true + - field: req_type + type: 1 + i18n: + zh-CN: 请求类型 + en-US: Req Type + ja-JP: リクエストタイプ + label: true + - field: value + type: 0 + i18n: + zh-CN: 总数 + en-US: Total + ja-JP: 総数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_adapter_requests_fail + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL + + - name: taos_adapter_requests_query + i18n: + zh-CN: 查询请求数 + en-US: Query Requests + ja-JP: クエリのリクエスト総数 + priority: 27 + fields: + - field: endpoint + type: 1 + i18n: + zh-CN: 端点 + en-US: Endpoint + ja-JP: エンドポイント + label: true + - field: req_type + type: 1 + i18n: + zh-CN: 请求类型 + en-US: Req Type + ja-JP: リクエストタイプ + label: true + - field: value + type: 0 + i18n: + zh-CN: 总数 + en-US: Total + ja-JP: 総数 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: ^_^uri^_^ + method: ^_^method^_^ + ssl: ^_^ssl^_^ + payload: ^_^payload^_^ + headers: + content-type: ^_^contentType^_^ + ^_^headers^_^: ^_^headers^_^ + params: + query: taos_adapter_requests_query + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: PromQL diff --git a/hertzbeat-manager/src/main/resources/define/app-tidb.yml b/hertzbeat-manager/src/main/resources/define/app-tidb.yml new file mode 100644 index 0000000..4d77efc --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-tidb.yml @@ -0,0 +1,352 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: tidb +# The monitoring i18n name +name: + zh-CN: TiDB数据库 + en-US: TiDB + ja-JP: TiDB データベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 HTTP 和 JDBC 协议对 TiDB 的通用性能指标进行采集监控。<br>您可以点击“<i>新建 TiDB 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat monitors general performance metrics of TiDB through HTTP and JDBC protocol. <br> You could click the "<i>New TiDB</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>". + zh-TW: HertzBeat 使用 HTTP 和 JDBC 協議對 TiDB 的通用性能指標進行採集監控。 <br>您可以點擊“<i>新建 TiDB 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は HTTP と JDBCプロトコルを介して TiDB データベースの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 TiDB データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/tidb + en-US: https://hertzbeat.apache.org/docs/help/tidb +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: JDBC 端口 + en-US: JDBC Port + ja-JP: JDBCポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + defaultValue: 4000 + # hide param-true or false + hide: true + - field: service-port + name: + zh-CN: Service 端口 + en-US: Service Port + ja-JP: サービスポート + type: number + range: '[0,65535]' + required: true + defaultValue: 10080 + + - field: pd-port + name: + zh-CN: PD 端口 + en-US: PD Port + ja-JP: PDポート + type: number + range: '[0,65535]' + required: false + defaultValue: 2379 + + - field: timeout + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + type: number + required: false + defaultValue: 6000 + + - field: database + name: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + type: text + required: false + hide: true + + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + hide: true + + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + hide: true + + - field: url + name: + zh-CN: JDBC URL + en-US: JDBC URL + ja-JP: JDBC URL + type: text + required: false + hide: true +# collect metrics config list +metrics: + # metrics - status + - name: status + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: ステータス + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: connections + type: 0 + i18n: + zh-CN: 连接数 + en-US: Connections + ja-JP: 接続数 + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + - field: git_hash + type: 1 + i18n: + zh-CN: Git Hash + en-US: Git Hash + ja-JP: Git Hash + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^service-port^_^ + # http url + url: '/status' + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH + method: GET + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: jsonPath + parseScript: '$' + + - name: stores + i18n: + zh-CN: 存储 + en-US: Stores + ja-JP: ストレージ + priority: 1 + fields: + - field: address + type: 1 + i18n: + zh-CN: 地址 + en-US: Address + ja-JP: アドレス + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + - field: status_address + type: 1 + i18n: + zh-CN: 状态地址 + en-US: Status Address + ja-JP: ステータスアドレス + - field: deploy_path + type: 1 + i18n: + zh-CN: 部署路径 + en-US: Deploy Path + ja-JP: デプロイパス + - field: state_name + type: 1 + i18n: + zh-CN: 状态名称 + en-US: State Name + ja-JP: 状態名 + - field: capacity + type: 1 + i18n: + zh-CN: 容量 + en-US: Capacity + ja-JP: 容量 + - field: available + type: 1 + i18n: + zh-CN: 可用 + en-US: Available + ja-JP: 使用可能 + - field: used_size + type: 1 + i18n: + zh-CN: 已用 + en-US: Used + ja-JP: 使用済み + - field: start_ts + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Start Time + ja-JP: 起動時間 + - field: last_heartbeat_ts + type: 1 + i18n: + zh-CN: 上次心跳时间 + en-US: Last Heartbeat Time + ja-JP: 最後のハートビート時間 + - field: uptime + type: 1 + i18n: + zh-CN: 启动时长 + en-US: Uptime + ja-JP: アップタイム + aliasFields: + - $.store.address + - $.store.version + - $.store.status_address + - $.store.deploy_path + - $.store.state_name + - $.status.capacity + - $.status.available + - $.status.used_size + - $.status.start_ts + - $.status.last_heartbeat_ts + - $.status.uptime + calculates: + - address=$.store.address + - version=$.store.version + - status_address=$.store.status_address + - deploy_path=$.store.deploy_path + - state_name=$.store.state_name + - capacity=$.status.capacity + - available=$.status.available + - used_size=$.status.used_size + - start_ts=$.status.start_ts + - last_heartbeat_ts=$.status.last_heartbeat_ts + - uptime=$.status.uptime + protocol: http + http: + host: ^_^host^_^ + port: ^_^pd-port^_^ + url: '/pd/api/v1/stores' + timeout: ^_^timeout^_^ + method: GET + parseType: jsonPath + parseScript: '$.stores.*' + + - name: basic + i18n: + zh-CN: 基本信息 + en-US: Basic Info + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: version + type: 1 + i18n: + zh-CN: 版本 + en-US: Version + ja-JP: バージョン + label: true + - field: port + type: 1 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: datadir + type: 1 + i18n: + zh-CN: 数据目录 + en-US: Data Directory + ja-JP: データディレクトリ + - field: max_connections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - version + - port + - datadir + - max_connections + # (optional)mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - port=port + - datadir=datadir + - max_connections=max_connections + - version=version+"_"+version_compile_os+"_"+version_compile_machine + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # host: ipv4 ipv6 host + host: ^_^host^_^ + # port + port: ^_^port^_^ + # database platform name + platform: mysql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + # sql + sql: show global variables where Variable_name like 'version%' or Variable_name = 'max_connections' or Variable_name = 'datadir' or Variable_name = 'port'; + # JDBC url + url: ^_^url^_^ + diff --git a/hertzbeat-manager/src/main/resources/define/app-tomcat.yml b/hertzbeat-manager/src/main/resources/define/app-tomcat.yml new file mode 100644 index 0000000..38b4b26 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-tomcat.yml @@ -0,0 +1,447 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: webserver +# The monitoring type eg: linux windows tomcat mysql aws... +app: tomcat +# The monitoring i18n name +name: + zh-CN: Tomcat应用服务器 + en-US: Tomcat WebServer + ja-JP: Tomcat WebServer +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a href="https://hertzbeat.apache.org/docs/advanced/extend-jmx">JMX 协议</a> 对 Tomcat 的通用性能指标(基础信息,内存池,类加载,线程信息等)进行采集监控。<br>⚠️注意:您需要在 Tomcat 应用中开启 JMX 服务,应用启动时添加 JMX 参数, 可自定义暴露端口,对外IP。<a href="https://hertzbeat.apache.org/zh-cn/docs/help/tomcat">点击查看开启步骤</a>。 + en-US: HertzBeat uses <a href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'>JMX Protocol</a> to monitoring and collect general performance metric of Tomcat. <br>⚠️Note:You need to enable JMX services in Tomcat application, and add the JXM parameters when the application start. You can also customize external IP address and exposed port.<a href='https://hertzbeat.apache.org/docs/help/tomcat'>Click here to view the activation steps.</a> + zh-TW: HertzBeat 使用 <a href="https://hertzbeat.apache.org/docs/advanced/extend-jmx">JMX 協議</a> 對 Tomcat 的通用性能指標(基礎信息,內存池,類加載,線程信息等)進行采集監控。<br>⚠️注意:您需要在 Tomcat 應用中開啓 JMX 服務,應用啓動時添加 JMX 參數, 可自定義暴露端口,對外IP。<a href="https://hertzbeat.apache.org/zh-cn/docs/help/tomcat">點擊查看開啓步驟</a>。 + ja-JP: HertzBeat は <a href="https://hertzbeat.apache.org/docs/advanced/extend-jmx"> JMXプロトコルを介して</a> Tomcat の一般的なパフォーマンスのメトリクスを監視します。⚠️注意:Tomcat応用 で JMXサービス を有効にする必要があります。<a href="https://hertzbeat.apache.org/docs/help/tomcat">クリックしてガイドを見ます</a>。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/tomcat + en-US: https://hertzbeat.apache.org/docs/help/tomcat +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 9999 + # field-param field key + - field: url + # name-param field display i18n name + name: + zh-CN: JMX URL + en-US: JMX URL + ja-JP: JMX URL + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + # param field input placeholder + placeholder: 'service:jmx:rmi:///jndi/rmi://host:port/jmxrmi' + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - server + - name: server + i18n: + zh-CN: Tomcat 信息 + en-US: Tomcat Server + ja-JP: Tomcatサーバー + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: serverInfo + type: 1 + i18n: + zh-CN: 服务信息 + en-US: Server Info + ja-JP: サーバー情報 + - field: serverNumber + type: 1 + i18n: + zh-CN: 服务版本 + en-US: Server Number + ja-JP: サーバーバージョン + - field: serverBuilt + type: 1 + i18n: + zh-CN: Server Built + en-US: Server Built + ja-JP: サーバービルド + - field: port + type: 1 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: address + type: 1 + i18n: + zh-CN: 地址 + en-US: Address + ja-JP: アドレス + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jmx + # the config content when protocol is http + jmx: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # jmx username + username: ^_^username^_^ + # jmx password + password: ^_^password^_^ + # jmx mbean object name + objectName: Catalina:type=Server + # jmx url + url: ^_^url^_^ + + - name: memory_pool + priority: 1 + i18n: + zh-CN: 内存池 + en-US: Memory Pool + ja-JP: メモリプール + fields: + - field: name + type: 1 + label: true + i18n: + zh-CN: 指标名称 + en-US: Name + ja-JP: 名前 + - field: committed + type: 0 + unit: MB + i18n: + zh-CN: 已分配内存 + en-US: Committed + ja-JP: コミットされたメモリ + - field: init + type: 0 + unit: MB + i18n: + zh-CN: 初始化内存 + en-US: Init + ja-JP: イニシャルメモリ + - field: max + type: 0 + unit: MB + i18n: + zh-CN: 最大内存 + en-US: Max + ja-JP: 最大メモリ + - field: used + type: 0 + unit: MB + i18n: + zh-CN: 已使用内存 + en-US: Used + ja-JP: 使用したメモリ + units: + - committed=B->MB + - init=B->MB + - max=B->MB + - used=B->MB + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - Name + - Usage->committed + - Usage->init + - Usage->max + - Usage->used + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - name=Name + - committed=Usage->committed + - init=Usage->init + - max=Usage->max + - used=Usage->used + protocol: jmx + jmx: + # host: ipv4 ipv6 domain + host: ^_^host^_^ + # port + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=MemoryPool,name=* + url: ^_^url^_^ + + - name: class_loading + priority: 2 + i18n: + zh-CN: 类加载信息 + en-US: Class Loading + ja-JP: クラスローディング情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: LoadedClassCount + type: 0 + i18n: + zh-CN: 当前已加载类数量 + en-US: Loaded Class Count + ja-JP: ロードされたクラス数 + - field: TotalLoadedClassCount + type: 0 + i18n: + zh-CN: 已加载类总数量 + en-US: Total Loaded Class Count + ja-JP: ロードされたクラス総数 + - field: UnloadedClassCount + type: 0 + i18n: + zh-CN: 未加载类总数量 + en-US: Unloaded Class Count + ja-JP: アンロードされたクラス総数 + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=ClassLoading + url: ^_^url^_^ + + - name: thread + priority: 3 + i18n: + zh-CN: 线程信息 + en-US: Thread + ja-JP: スレッド情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: TotalStartedThreadCount + type: 0 + i18n: + zh-CN: 已启动线程总数 + en-US: Total Started Thread Count + ja-JP: スレッド総数 + - field: ThreadCount + type: 0 + i18n: + zh-CN: 活跃线程数 + en-US: Thread Count + ja-JP: 活躍スレッド数 + - field: PeakThreadCount + type: 0 + i18n: + zh-CN: 最大峰值线程数 + en-US: Peak Thread Count + ja-JP: 最大スレッド数 + - field: DaemonThreadCount + type: 0 + i18n: + zh-CN: 活跃守护线程数 + en-US: Daemon Thread Count + ja-JP: デーモンスレッド数 + - field: CurrentThreadUserTime + type: 0 + unit: s + i18n: + zh-CN: 线程占用的CPU时间(用户态) + en-US: Current Thread User Time + ja-JP: 現在のスレッドユーザー時間 + - field: CurrentThreadCpuTime + type: 0 + unit: s + i18n: + zh-CN: 线程占用的CPU时间 + en-US: Current Thread CPU Time + ja-JP: 現在のスレッドシステム時間 + units: + - CurrentThreadUserTime=NS->S + - CurrentThreadCpuTime=NS->S + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: java.lang:type=Threading + url: ^_^url^_^ + + - name: application + i18n: + zh-CN: 应用信息 + en-US: Application + ja-JP: 応用情報 + priority: 4 + fields: + - field: name + type: 1 + label: true + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: acceptCount + type: 0 + i18n: + zh-CN: 接收请求数 + en-US: Accept Count + ja-JP: 受け入れたリクエスト数 + - field: acceptorThreadCount + type: 0 + i18n: + zh-CN: 接收处理线程数 + en-US: Acceptor Thread Count + ja-JP: アクセプタスレッド数 + - field: acceptorThreadPriority + type : 0 + i18n: + zh-CN: 接收线程级别 + en-US: Acceptor Thread Priority + ja-JP: アクセプタスレッド優先度 + - field: connectionTimeout + type: 0 + unit: ms + i18n: + zh-CN: 链接超时时间 + en-US: Connection Timeout + ja-JP: 接続のタイムアウト + - field: currentThreadCount + type: 0 + i18n: + zh-CN: 当前线程数 + en-US: Current Thread Count + ja-JP: 現在のスレッド数 + - field: currentThreadsBusy + type: 0 + i18n: + zh-CN: 繁忙线程数 + en-US: Current Threads Busy + ja-JP: 現在の忙しいスレッド数 + - field: keepAliveCount + type: 0 + i18n: + zh-CN: 心跳保持链接数量 + en-US: Keep Alive Count + ja-JP: ハートビート数量 + - field: keepAliveTimeout + type: 0 + unit: ms + i18n: + zh-CN: 心跳超时时间 + en-US: Keep Alive Timeout + ja-JP: ハートビートのタイムアウト + - field: maxConnections + type: 0 + i18n: + zh-CN: 最大链接数 + en-US: Max Connections + ja-JP: 最大接続数 + - field: maxKeepAliveRequests + type: 0 + i18n: + zh-CN: 最大链接保持请求 + en-US: Max KeepAlive Requests + ja-JP: 最大KeepAliveリクエスト数 + - field: maxThreads + type: 0 + i18n: + zh-CN: 最大线程数 + en-US: Max Threads + ja-JP: 最大スレッド数 + - field: minSpareThreads + type: 0 + i18n: + zh-CN: 最小备用线程数 + en-US: Min Spare Threads + ja-JP: 最小予備スレッド数 + - field: pollerThreadCount + type: 0 + i18n: + zh-CN: 轮询线程数 + en-US: Poller Thread Count + ja-JP: ポーラースレッド数 + - field: pollerThreadPriority + type: 0 + i18n: + zh-CN: 轮询线程级别 + en-US: Poller Thread Priority + ja-JP: ポーラースレッド優先度 + - field: running + type: 1 + i18n: + zh-CN: 运行中 + en-US: Running + ja-JP: 実行中 + protocol: jmx + jmx: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + objectName: Catalina:type=ThreadPool,name=* + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml b/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml new file mode 100644 index 0000000..5d469f7 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml @@ -0,0 +1,462 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: network +# The monitoring type eg: linux windows tomcat mysql aws... +app: tplink_switch +# The monitoring i18n name +name: + zh-CN: TP-LINK通用交换机 + en-US: TP-LINK Switch + ja-JP: TP-LINKスイッチングハブ +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 协议</a> 对 TP-LINK交换机 的通用指标(可用性,系统信息,端口流量等)进行采集监控。<br>您可以点击 “<i>新建 TP-LINK通用交换机</i>” 并进行配置SNMP相关参数添加,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP Protocol</a> to monitoring TP-LINK Switch general performance metrics. <br>You can click the "<i>New TP-LINK Switch</i>" button and config snmp params to add monitor or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 協議</a> 對 TP-LINK交換機 的通用指標(可用性,系統信息,端口流量等)進行采集監控。<br>您可以點擊 “<i>新建 TP-LINK通用交換機</i>” 並進行配置SNMP相關參數添加,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP プロトコルを介して</a> TP-LINKスイッチングハブの一般的なメトリクスを監視します。<br>「<i>新規 TP-LINKスイッチングハブ</i>」をクリックしてSNMPなどのパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/tplink_switch + en-US: https://hertzbeat.apache.org/docs/help/tplink_switch +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 161 + # field-param field key + - field: snmpVersion + # name-param field display i18n name + name: + zh-CN: SNMP 版本 + en-US: SNMP Version + ja-JP: SNMPバージョン + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: SNMPv1 + value: 0 + - label: SNMPv2c + value: 1 + - label: SNMPv3 + value: 3 + # field-param field key + - field: community + # name-param field display i18n name + name: + zh-CN: SNMP 团体字 + en-US: SNMP Community + ja-JP: SNMPコミュニティ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp community for v1 v2c' + # dependent parameter values list + depend: + snmpVersion: + - 0 + - 1 + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: SNMP username + en-US: SNMP username + ja-JP: SNMPユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp username for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: contextName + # name-param field display i18n name + name: + zh-CN: SNMP contextName + en-US: SNMP contextName + ja-JP: SNMPコンテキスト名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp contextName for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP authPassword + en-US: SNMP authPassword + ja-JP: SNMP認証パスワード + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: authPasswordEncryption + # name-param field display i18n name + name: + zh-CN: authPassword 加密方式 + en-US: authPassword Encryption + ja-JP: 認証暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: MD5 + value: 0 + - label: SHA1 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPassphrase + # name-param field display i18n name + name: + zh-CN: SNMP privPassphrase + en-US: SNMP privPassphrase + ja-JP: SNMPパスワードフレーズ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Snmp authPassword for v3' + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: privPasswordEncryption + # name-param field display i18n name + name: + zh-CN: privPassword 加密方式 + en-US: privPassword Encryption + ja-JP: パスワードの暗号 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: DES + value: 0 + - label: AES128 + value: 1 + # dependent parameter values list + depend: + snmpVersion: + - 3 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: false + # hide-is hide this field and put it in advanced layout + hide: true + # default value + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - system + - name: system + i18n: + zh-CN: 系统信息 + en-US: System Info + ja-JP: システム情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: name + type: 1 + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: descr + type: 1 + i18n: + zh-CN: 描述信息 + en-US: Description + ja-JP: 説明 + - field: uptime + type: 1 + i18n: + zh-CN: 运行时长 + en-US: Uptime + ja-JP: アップタイム + - field: location + type: 1 + i18n: + zh-CN: 位置 + en-US: Location + ja-JP: 位置 + - field: contact + type: 1 + i18n: + zh-CN: 联系人 + en-US: Contact + ja-JP: 連絡先 + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: snmp + # the config content when protocol is snmp + snmp: + # server host: ipv4 ipv6 domain + host: ^_^host^_^ + # server port + port: ^_^port^_^ + # snmp username + username: ^_^username^_^ + # snmp authPassphrase + authPassphrase: ^_^authPassphrase^_^ + # snmp privPassphrase + privPassphrase: ^_^privPassphrase^_^ + # snmp privPasswordEncryption + privPasswordEncryption: ^_^privPasswordEncryption^_^ + # snmp authPasswordEncryption + authPasswordEncryption: ^_^authPasswordEncryption^_^ + # contextName + contextName: ^_^contextName^_^ + # snmp connect timeout + timeout: ^_^timeout^_^ + # snmp community + community: ^_^community^_^ + # snmp version + version: ^_^snmpVersion^_^ + # snmp operation: get, walk + operation: get + # metrics oids: metric_name - oid_value + oids: + name: 1.3.6.1.2.1.1.5.0 + descr: 1.3.6.1.2.1.1.1.0 + uptime: 1.3.6.1.2.1.1.3.0 + location: 1.3.6.1.2.1.1.6.0 + contact: 1.3.6.1.2.1.1.4.0 + + - name: interfaces + i18n: + zh-CN: 接口详情 + en-US: Interfaces Detail + ja-JP: ネットワークカード詳細 + priority: 1 + fields: + - field: index + type: 1 + i18n: + zh-CN: 编号 + en-US: Index + ja-JP: 番号 + - field: descr + type: 1 + label: true + i18n: + zh-CN: 接口名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: mtu + type: 0 + unit: 'byte' + i18n: + zh-CN: MTU + en-US: MTU + ja-JP: MTU + - field: speed + type: 0 + unit: 'MB/s' + i18n: + zh-CN: 接口速率 + en-US: Interface Speed + ja-JP: ネットワークカード速度 + - field: in_octets + type: 0 + unit: 'MByte' + i18n: + zh-CN: 入流量 + en-US: In Octets + ja-JP: 受信されたバイト数 + - field: in_discards + type: 0 + i18n: + zh-CN: 入丢包数 + en-US: In Discards + ja-JP: 受信されたパケットのロス数 + - field: in_errors + type: 0 + i18n: + zh-CN: 入错包数 + en-US: In Errors + ja-JP: 受信異常パケット数 + - field: out_octets + type: 0 + unit: 'MByte' + i18n: + zh-CN: 出流量 + en-US: Out Octets + ja-JP: 転送されたバイト数 + - field: out_discards + type: 0 + i18n: + zh-CN: 出丢包数 + en-US: Out Discards + ja-JP: 転送されたパケットのロス数 + - field: out_errors + type: 0 + i18n: + zh-CN: 出错包数 + en-US: Out Errors + ja-JP: 転送異常パケット数 + - field: admin_status + type: 1 + i18n: + zh-CN: 配置状态 + en-US: Config Status + ja-JP: 設定ステータス + - field: oper_status + type: 1 + i18n: + zh-CN: 当前状态 + en-US: Current Status + ja-JP: 現在のステータス + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - ifIndex + - ifDescr + - ifMtu + - ifSpeed + - ifInOctets + - ifInDiscards + - ifInErrors + - ifOutOctets + - ifOutDiscards + - ifOutErrors + - ifAdminStatus + - ifOperStatus + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - index=ifIndex + - descr=ifDescr + - mtu=ifMtu + - speed=ifSpeed / 1000 / 1000 + - in_octets=ifInOctets / 1000 / 1000 + - in_discards=ifInDiscards + - in_errors=ifInErrors + - out_octets=ifOutOctets / 1000 / 1000 + - out_discards=ifOutDiscards + - out_errors=ifOutErrors + - admin_status=ifAdminStatus + - oper_status=ifOperStatus + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + authPassphrase: ^_^authPassphrase^_^ + privPassphrase: ^_^privPassphrase^_^ + privPasswordEncryption: ^_^privPasswordEncryption^_^ + authPasswordEncryption: ^_^authPasswordEncryption^_^ + contextName: ^_^contextName^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + ifIndex: 1.3.6.1.2.1.2.2.1.1 + ifDescr: 1.3.6.1.2.1.2.2.1.2 + ifMtu: 1.3.6.1.2.1.2.2.1.4 + ifSpeed: 1.3.6.1.2.1.2.2.1.5 + ifInOctets: 1.3.6.1.2.1.2.2.1.10 + ifInDiscards: 1.3.6.1.2.1.2.2.1.13 + ifInErrors: 1.3.6.1.2.1.2.2.1.14 + ifOutOctets: 1.3.6.1.2.1.2.2.1.16 + ifOutDiscards: 1.3.6.1.2.1.2.2.1.19 + ifOutErrors: 1.3.6.1.2.1.2.2.1.20 + ifAdminStatus: 1.3.6.1.2.1.2.2.1.7 + ifOperStatus: 1.3.6.1.2.1.2.2.1.8 diff --git a/hertzbeat-manager/src/main/resources/define/app-ubuntu.yml b/hertzbeat-manager/src/main/resources/define/app-ubuntu.yml new file mode 100644 index 0000000..1b7c8ca --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-ubuntu.yml @@ -0,0 +1,765 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: ubuntu +# The monitoring i18n name +name: + zh-CN: Ubuntu Linux + en-US: Ubuntu Linux + ja-JP: Ubuntu Linux +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 Ubuntu 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Ubuntu</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors Ubuntu operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New Ubuntu</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 Ubuntu 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Ubuntu</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> Ubuntuシステムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Ubuntu</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/ubuntu + en-US: https://hertzbeat.apache.org/docs/help/ubuntu +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 22 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # default value + defaultValue: 6000 + # field-param field key + - field: reuseConnection + # name-param field display i18n name + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: true + # field-param field key + - field: useProxy + # name-param field display i18n name + name: + zh-CN: 使用代理 + en-US: Use Proxy Connection + ja-JP: プロキシ接続利用 + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + defaultValue: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + - field: privateKey + # name-param field display i18n name + name: + zh-CN: 私钥 + en-US: PrivateKey + ja-JP: 秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: privateKeyPassphrase + # name-param field display i18n name + name: + zh-CN: 密钥短语 + en-US: PrivateKey PassPhrase + ja-JP: 秘密鍵フレーズ + # type-param field type(most mapping the html input type) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyHost + # name-param field display i18n name + name: + zh-CN: 代理主机 + en-US: Proxy Host + ja-JP: プロキシホスト + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: false + # hide param-true or false + hide: true + - field: proxyPort + # name-param field display i18n name + name: + zh-CN: 代理端口 + en-US: Proxy Port + ja-JP: プロキシポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 22 + # field-param field key + - field: proxyUsername + # name-param field display i18n name + name: + zh-CN: 代理用户名 + en-US: Proxy Username + ja-JP: プロキシユーザー名 + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPassword + # name-param field display i18n name + name: + zh-CN: 代理密码 + en-US: Proxy Password + ja-JP: プロキシパスワード + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: proxyPrivateKey + # name-param field display i18n name + name: + zh-CN: 代理主机私钥 + en-US: proxyPrivateKey + ja-JP: プロキシ秘密鍵 + # type-param field type(most mapping the html input type) + type: textarea + placeholder: -----BEGIN RSA PRIVATE KEY----- + # required-true or false + required: false + # hide param-true or false + hide: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: システム基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + - field: uptime + type: 1 + i18n: + zh-CN: 启动时间 + en-US: Uptime + ja-JP: アップタイム + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: ssh + # the config content when protocol is ssh + ssh: + # ssh host: ipv4 ipv6 domain + host: ^_^host^_^ + # ssh port + port: ^_^port^_^ + # ssh username + username: ^_^username^_^ + # ssh password + password: ^_^password^_^ + # ssh private key + privateKey: ^_^privateKey^_^ + # ssh private key passphrase + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + # ssh run collect script + script: (uname -r ; hostname ; uptime | awk -F "," '{print $1}' | sed "s/ //g") | sed ":a;N;s/\n/^/g;ta" | awk -F '^' 'BEGIN{print "version hostname uptime"} {print $1, $2, $3}' + # ssh response data parse type: oneRow, multiRow + parseType: multiRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: interrupt + type: 0 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: load + type: 1 + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: context_switch + type: 0 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - info + - cores + - load + - interrupt + - context_switch + - idle + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - info=info + - cores=cores + - load=load + - interrupt=interrupt + - context_switch=context_switch + - usage=100-idle + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: "LANG=C lscpu | awk -F: '$1==\"Model name\" {print $2}';awk '/processor/{core++} END{print core}' /proc/cpuinfo;uptime | sed 's/,/ /g' | awk '{for(i=NF-2;i<=NF;i++)print $i }' | xargs;vmstat 1 1 | awk 'NR==3{print $11}';vmstat 1 1 | awk 'NR==3{print $12}';vmstat 1 2 | awk 'NR==4{print $15}'" + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: total + type: 1 + unit: Mb + i18n: + zh-CN: 总内存容量 + en-US: Total Memory + ja-JP: メモリ容量 + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 用户程序内存量 + en-US: User Program Memory + ja-JP: ユーザープログラムメモリ + - field: free + type: 0 + unit: Mb + i18n: + zh-CN: 空闲内存容量 + en-US: Free Memory + ja-JP: 空きメモリ + - field: buff_cache + type: 0 + unit: Mb + i18n: + zh-CN: 缓存占用内存 + en-US: Buff Cache Memory + ja-JP: バッファメモリ + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 剩余可用内存 + en-US: Available Memory + ja-JP: 使用可能のメモリ + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存使用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + aliasFields: + - total + - used + - free + - buff_cache + - available + calculates: + - total=total + - used=used + - free=free + - buff_cache=buff_cache + - available=available + - usage=(used / total) * 100 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: free -m | awk 'BEGIN{print "total used free buff_cache available"} NR==2{print $2,$3,$4,$6,$7}' + parseType: multiRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: disk_num + type: 1 + i18n: + zh-CN: 磁盘总数 + en-US: Disk Num + ja-JP: ディスク番号 + - field: partition_num + type: 1 + i18n: + zh-CN: 分区总数 + en-US: Partition Num + ja-JP: パーティション + - field: block_write + type: 0 + i18n: + zh-CN: 写磁盘块数 + en-US: Block Write + ja-JP: 書き込みディスクブロック数 + - field: block_read + type: 0 + i18n: + zh-CN: 读磁盘块数 + en-US: Block Read + ja-JP: 読み取りブロック数 + - field: write_rate + type: 0 + unit: iops + i18n: + zh-CN: 磁盘写速率 + en-US: Write Rate + ja-JP: ディスク書き込み速度 + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: vmstat -D | awk 'NR==1{print $1}';vmstat -D | awk 'NR==2{print $1}';vmstat 1 1 | awk 'NR==3{print $10}';vmstat 1 1 | awk 'NR==3{print $9}';vmstat 1 1 | awk 'NR==3{print $16}' + parseType: oneRow + + - name: interface + i18n: + zh-CN: 网卡信息 + en-US: Interface Info + ja-JP: ネットワークカード情報 + priority: 4 + fields: + - field: interface_name + type: 1 + label: true + i18n: + zh-CN: 网卡名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: receive_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 入站数据流量 + en-US: Receive Bytes + ja-JP: 受信されたバイト数 + - field: transmit_bytes + type: 0 + unit: Mb + i18n: + zh-CN: 出站数据流量 + en-US: Transmit Bytes + ja-JP: 転送されたバイト数 + units: + - receive_bytes=B->MB + - transmit_bytes=B->MB + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: cat /proc/net/dev | tail -n +3 | awk 'BEGIN{ print "interface_name receive_bytes transmit_bytes"} {print $1,$2,$10}' + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 5 + fields: + - field: filesystem + type: 1 + i18n: + zh-CN: 文件系统 + en-US: Filesystem + ja-JP: ファイルシステム + - field: used + type: 0 + unit: Mb + i18n: + zh-CN: 已使用量 + en-US: Used + ja-JP: 使用済み + - field: available + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: Available + ja-JP: 使用可能 + - field: usage + type: 0 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + - field: mounted + type: 1 + label: true + i18n: + zh-CN: 挂载点 + en-US: Mounted + ja-JP: マウント + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: df -mP | tail -n +2 | awk 'BEGIN{ print "filesystem used available usage mounted"} {print $1,$3,$4,$5,$6}' + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 6 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k3nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 7 + fields: + - field: pid + type: 1 + label: true + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: mem_usage + type: 0 + unit: '%' + i18n: + zh-CN: 内存占用率 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu_usage + type: 0 + unit: '%' + i18n: + zh-CN: CPU占用率 + en-US: CPU Usage + ja-JP: CPU使用率 + - field: command + type: 1 + i18n: + zh-CN: 执行命令 + en-US: Command + ja-JP: コマンド + protocol: ssh + ssh: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + privateKey: ^_^privateKey^_^ + privateKeyPassphrase: ^_^privateKeyPassphrase^_^ + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + # whether to use proxy server for ssh connection + useProxy: ^_^useProxy^_^ + # ssh proxy host: ipv4 domain + proxyHost: ^_^proxyHost^_^ + # ssh proxy port + proxyPort: ^_^proxyPort^_^ + # ssh proxy username + proxyUsername: ^_^proxyUsername^_^ + # ssh proxy password + proxyPassword: ^_^proxyPassword^_^ + # ssh proxy private key + proxyPrivateKey: ^_^proxyPrivateKey^_^ + script: ps aux | sort -k4nr | awk 'BEGIN{ print "pid cpu_usage mem_usage command" } {printf "%s %s %s ", $2, $3, $4; for (i=11; i<=NF; i++) { printf "%s", $i; if (i < NF) printf " "; } print ""}' | head -n 11 + parseType: multiRow \ No newline at end of file diff --git a/hertzbeat-manager/src/main/resources/define/app-udp_port.yml b/hertzbeat-manager/src/main/resources/define/app-udp_port.yml new file mode 100644 index 0000000..0bb8f4b --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-udp_port.yml @@ -0,0 +1,117 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: udp_port +# The monitoring i18n name +name: + zh-CN: UDP端口可用性 + en-US: UDP PORT + ja-JP: UDP PORT +# The description and help of this monitoring type +help: + zh-CN: UDP 是面向无连接的传输层协议,其端口可用性状态我们通过在应用层发送请求报文获得响应来判断,配置信息我们需要填充能使对端响应的应用层16进制报文内容。建议使用wireshark抓包来获取发送报文内容。<br>您可以点击“<i>新建 UDP端口可用性</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: UDP is a connectionless transport layer protocol. We can judge its port availability status by sending a request message at the application layer and getting a response. We need to input the application layer hexadecimal message content that enables the peer to respond. Suggest using wireshark to capture packets to obtain the send package content. <br>You could click the "<i>New UDP Port</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: UDP 是面向無連接的傳輸層協議,其連接埠可用性狀態我們透過在應用層發送請求封包獲得回應來判斷,設定指標我們需要填充能使對端回應的應用層16進位封包 內容。建議使用wireshark抓包來取得報文內容。<br>您可以點選“<i>新 UDP連接埠可用性</i>”並進行配置,或選擇“<i>更多操作</i>”,匯入已有配置。 + ja-JP: UDPはコネクションレスのトランスポート層プロトコルであり、そのポートの使用可能状態はアプリケーション層でリクエスト・メッセージを送信して応答を得ることで決定される。ピアから応答が得られるようにするためには、アプリケーション層の16進メッセージの内容で設定情報を埋める必要があります。送信されたメッセージの内容を取得するには、wireshark のパケットキャプチャを使用することを推奨します。<br>「<i>新規 UDP PORT</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/port + en-US: https://hertzbeat.apache.org/docs/help/port +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value 81 + defaultValue: 81 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value 6000 + defaultValue: 6000 + # field-param field key + - field: content + name: + zh-CN: 发送报文内容(16进制串) + en-US: Send Package Content(HexString) + ja-JP: パッケージ内容(HexString) + type: text + placeholder: 3025020101040 + required: false +# collect metrics config list +metrics: + # metrics - summary + - name: summary + i18n: + zh-CN: 概要 + en-US: Summary + ja-JP: 概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: udp + # Specific collection configuration when protocol is telnet protocol + udp: + # telnet host + host: ^_^host^_^ + # port + port: ^_^port^_^ + # timeout + timeout: ^_^timeout^_^ + # protocol content + content: ^_^content^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-uniview.yml b/hertzbeat-manager/src/main/resources/define/app-uniview.yml new file mode 100644 index 0000000..306a165 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-uniview.yml @@ -0,0 +1,172 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: server +# The monitoring type eg: linux windows tomcat mysql aws... +app: uniview +# The monitoring i18n name +name: + zh-CN: 宇视 + en-US: Uniview + ja-JP: Uniview +# The description and help of this monitoring type +help: + zh-CN: 通过HTTP接口监控宇视设备状态,获取设备健康数据。 + en-US: Monitor Uniview devices through HTTP interface to collect health data. + ja-JP: Hertzbeat は HTTPインターフェースを呼び出してUniviewデバイスの健全データを収集します。 + +# Input params define for monitoring(render web ui by the definition) +params: + - field: host + name: + zh-CN: 主机Host + en-US: Host + ja-JP: ホスト + type: host + required: true + - field: port + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + type: number + range: '[0,65535]' + required: true + defaultValue: 80 + - field: timeout + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + type: number + range: '[1000,60000]' + required: true + defaultValue: 5000 + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + required: true + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: true + - field: ssl + name: + zh-CN: 启用HTTPS + en-US: SSL + ja-JP: SSL + type: boolean + required: false + defaultValue: false + +# collect metrics config list +metrics: + - name: system_info + i18n: + zh-CN: 系统信息 + en-US: System Info + ja-JP: システム情報 + priority: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + ssl: ^_^ssl^_^ + url: /LAPI/V1.0/System/DeviceInfo + method: GET + timeout: ^_^timeout^_^ + authorization: + type: Digest Auth + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.Response.Data' + fields: + - field: DeviceName + type: 1 + i18n: + zh-CN: 设备名称 + en-US: Device Name + ja-JP: デバイス名 + - field: SerialNumber + type: 1 + i18n: + zh-CN: 序列号 + en-US: SerialNumber + ja-JP: シリアル番号 + - field: FirmwareVersion + type: 1 + i18n: + zh-CN: 固件版本 + en-US: Firmware Version + ja-JP: ファームウェアバージョン + - field: DeviceModel + type: 1 + i18n: + zh-CN: 设备型号 + en-US: Device Model + ja-JP: デバイスモデル + - name: ntp_info + i18n: + zh-CN: 校时信息 + en-US: NTP Info + ja-JP: NTP情報 + priority: 0 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + ssl: ^_^ssl^_^ + url: /LAPI/V1.0/System/Time/NTP + method: GET + timeout: ^_^timeout^_^ + authorization: + type: Digest Auth + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + parseType: jsonPath + parseScript: '$.Response.Data.NTPServerInfos[0]' + fields: + - field: IPAddress + type: 1 + i18n: + zh-CN: IP地址 + en-US: IP Address + ja-JP: IP アドレス + - field: Port + type: 1 + i18n: + zh-CN: 端口号 + en-US: Port + ja-JP: ポート + - field: SynchronizeInterval + type: 1 + i18n: + zh-CN: 同步间隔 + en-US: SynchronizeInterval + ja-JP: 同期間隔 + - field: Enabled + type: 1 + i18n: + zh-CN: 是否启用 + en-US: Enabled + ja-JP: 有効化 \ No newline at end of file diff --git a/hertzbeat-manager/src/main/resources/define/app-valkey.yml b/hertzbeat-manager/src/main/resources/define/app-valkey.yml new file mode 100644 index 0000000..d949ac5 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-valkey.yml @@ -0,0 +1,1468 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: cache +# The monitoring type eg: linux windows tomcat mysql aws... +app: valkey +# The monitoring i18n name +name: + zh-CN: Valkey数据库 + en-US: Valkey + ja-JP: Valkeyデータベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 Valkey 数据库的通用性能指标进行采集监控(server、clients、memory、persistence、stats、replication、cpu、errorstats、cluster、commandstats),支持版本为 Valkey 7.0+。<br>您可以点击“<i>新建 Valkey 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat monitors Valkey database of general performance metrics such as memory, persistence, replication and so on. The versions we support is Valkey 7.0+. <br>You could click the "<i>New Valkey</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: HertzBeat 對 Valkey 數據庫的通用性能指標進行采集監控(server、clients、memory、persistence、stats、replication、cpu、errorstats、cluster、commandstats),支持版本爲 Valkey 7.0+。<br>您可以點擊“<i>新建 Valkey 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は Valkey データベース(Valkey 7.0+)の一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Valkey データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/valkey + en-US: https://hertzbeat.apache.org/docs/help/valkey +# Input params define for monitoring(render web ui by +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 6379 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间 + en-US: Timeout + ja-JP: タイムアウト + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: true + # default value + defaultValue: 3000 + # field-param field key + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + # field-param field key + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + +# collect metrics config list +metrics: + # metrics - server + - name: server + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + i18n: + zh-CN: 服务器信息 + en-US: Server + ja-JP: サーバー情報 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: identity + type: 1 + i18n: + zh-CN: ID + en-US: Identity + ja-JP: ID + - field: valkey_version + type: 1 + i18n: + zh-CN: Valkey服务版本 + en-US: Valkey Version + ja-JP: Valkeyバージョン + - field: valkey_git_sha1 + type: 0 + i18n: + zh-CN: Git SHA1 + en-US: Git SHA1 + ja-JP: Git SHA1 + - field: valkey_git_dirty + type: 0 + i18n: + zh-CN: Valkey服务器代码是否有改动 + en-US: Git Dirty Flag + ja-JP: ValkeyサーバーのGitリポジトリ状態 + - field: valkey_build_id + type: 1 + i18n: + zh-CN: Build Id + en-US: Build Id + ja-JP: ビルド Id + - field: valkey_mode + type: 1 + i18n: + zh-CN: 运行模式 + en-US: Server Mode + ja-JP: サーバーモード + - field: os + type: 1 + i18n: + zh-CN: 操作系统 + en-US: Operating System + ja-JP: オーエス + - field: arch_bits + type: 0 + i18n: + zh-CN: 架构 + en-US: Architecture Bits + ja-JP: アーキテクチャ + - field: multiplexing_api + type: 1 + i18n: + zh-CN: IO多路复用器API + en-US: Multiplexing API + ja-JP: IO多重化API + - field: atomicvar_api + type: 1 + i18n: + zh-CN: 原子操作处理API + en-US: Atomicvar API + ja-JP: 原子操作API + - field: gcc_version + type: 1 + i18n: + zh-CN: GCC版本 + en-US: GCC Version + ja-JP: GCC バージョン + - field: process_id + type: 0 + i18n: + zh-CN: 进程ID + en-US: PID + ja-JP: プロセスID + - field: process_supervised + type: 1 + i18n: + zh-CN: 进程监督机制管理Valkey的方式 + en-US: Process Supervised + ja-JP: プロセスの監視方法 + - field: run_id + type: 1 + i18n: + zh-CN: Run ID + en-US: Run ID + ja-JP: Run ID + - field: tcp_port + type: 0 + i18n: + zh-CN: TCP/IP监听端口 + en-US: TCP Port + ja-JP: TCP ポート + - field: server_time_usec + type: 0 + i18n: + zh-CN: 服务器时间戳 + en-US: Server Time Usec + ja-JP: サーバーのタイムスタンプ + - field: uptime_in_seconds + type: 0 + i18n: + zh-CN: 运行时长(秒) + en-US: Uptime(Seconds) + ja-JP: アップタイム(秒) + - field: uptime_in_days + type: 0 + i18n: + zh-CN: 运行时长(天) + en-US: Uptime(Days) + ja-JP: アップタイム(日) + - field: hz + type: 0 + i18n: + zh-CN: 事件循环频率 + en-US: hz + ja-JP: イベントの実行頻度 + - field: configured_hz + type: 0 + i18n: + zh-CN: 配置的事件循环频率 + en-US: Configured hz + ja-JP: 設定されたイベントの実行頻度 + - field: lru_clock + type: 0 + i18n: + zh-CN: LRU时钟 + en-US: LRU Clock + ja-JP: LRU クロック + - field: executable + type: 1 + i18n: + zh-CN: 服务器执行路径 + en-US: Server's Executable Path + ja-JP: サーバーの実行パス + - field: config_file + type: 1 + i18n: + zh-CN: 配置文件路径 + en-US: Config File Path + ja-JP: 設定されたサーバーの実行ファイル + - field: io_threads_active + type: 0 + i18n: + zh-CN: 活跃IO线程数 + en-US: Active IO Threads + ja-JP: 活動中のスレッド数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + # metrics - clients + - name: clients + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + i18n: + zh-CN: 客户端信息 + en-US: Clients + ja-JP: クライアント情報 + # collect metrics content + fields: + - field: connected_clients + type: 0 + i18n: + zh-CN: 已连接客户端数量 + en-US: Connected Clients + ja-JP: 現在接続されているクライアントの数 + - field: cluster_connections + type: 0 + i18n: + zh-CN: 集群客户端连接数 + en-US: Cluster Connections + ja-JP: クラスター内の接続数 + - field: maxclients + type: 0 + i18n: + zh-CN: 最大客户端连接数 + en-US: Max Clients + ja-JP: 最大クライアント数 + - field: client_recent_max_input_buffer + type: 0 + i18n: + zh-CN: 最近客户端的最大输入缓冲区大小 + en-US: Client Recent Max Input Buffer + ja-JP: クライアントの最近の最大入力バッファサイズ + - field: client_recent_max_output_buffer + type: 0 + i18n: + zh-CN: 最近客户端的最大输出缓冲区大小 + en-US: Client Recent Max Output Buffer + ja-JP: クライアントの最近の最大出力バッファサイズ + - field: blocked_clients + type: 0 + i18n: + zh-CN: 阻塞客户端数量 + en-US: Blocked Clients + ja-JP: ブロックされたクライアント数 + - field: tracking_clients + type: 0 + i18n: + zh-CN: 正在追踪数据的客户端数量 + en-US: Tracking Clients + ja-JP: トラッキングを使用するクライアントの数 + - field: clients_in_timeout_table + type: 0 + i18n: + zh-CN: 超时队列中的客户端数量 + en-US: Clients In Timeout Table + ja-JP: タイムアウトテーブル内のクライアント数 + protocol: redis + redis: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + password: ^_^password^_^ + timeout: ^_^timeout^_^ + # metrics - memory + - name: memory + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + i18n: + zh-CN: 内存信息 + en-US: Memory + ja-JP: メモリ情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: used_memory + type: 0 + i18n: + zh-CN: 已使用内存(字节) + en-US: Used Memory + ja-JP: 使用したメモリ(バイト) + - field: used_memory_human + type: 0 + unit: MB + i18n: + zh-CN: 已使用内存 + en-US: Used Memory Human + ja-JP: 使用したメモリ + - field: used_memory_rss + type: 0 + i18n: + zh-CN: 已使用物理内存(字节) + en-US: Used Memory RSS + ja-JP: 使用した物理メモリ(バイト) + - field: used_memory_rss_human + type: 0 + unit: MB + i18n: + zh-CN: 已使用物理内存 + en-US: Used Memory RSS Human + ja-JP: 使用した物理メモリ + - field: used_memory_peak + type: 0 + i18n: + zh-CN: 内存使用最大峰值(字节) + en-US: Used Memory Peak + ja-JP: 使用したメモリの最大値(バイト) + - field: used_memory_peak_human + type: 0 + unit: MB + i18n: + zh-CN: 内存使用最大值 + en-US: Used Memory Peak Human + ja-JP: 使用したメモリの最大値 + - field: used_memory_peak_perc + type: 0 + unit: '%' + i18n: + zh-CN: 最大内存使用率 + en-US: Used Memory Peak Perc + ja-JP: メモリの最大使用率 + - field: used_memory_overhead + type: 0 + i18n: + zh-CN: 管理数据结构的额外内存使用量 + en-US: Used Memory Overhead + ja-JP: オーバーヘッドのメモリ使用量 + - field: used_memory_startup + type: 0 + i18n: + zh-CN: Valkey服务器启动时使用的内存 + en-US: Used Memory Startup + ja-JP: 起動時に使用したメモリ量 + - field: used_memory_dataset + type: 0 + i18n: + zh-CN: 存储数据占用的内存 + en-US: Used Memory Dataset + ja-JP: データセットが使用中のメモリ量 + - field: used_memory_dataset_perc + type: 0 + unit: '%' + i18n: + zh-CN: 存储数据占用的内存比率 + en-US: Used Memory Dataset Perc + ja-JP: データセットが使用中のメモリ率 + - field: allocator_allocated + type: 0 + i18n: + zh-CN: 内存分配器分配的内存 + en-US: Allocator Allocated + ja-JP: 実際に割り当てられたメモリ量 + - field: allocator_active + type: 0 + i18n: + zh-CN: 内存分配器激活的内存 + en-US: Allocator Active + ja-JP: メモリ割り当て器がオーエスに要求したメモリ量 + - field: allocator_resident + type: 0 + i18n: + zh-CN: 内存分配器加载的内存 + en-US: Allocator Resident + ja-JP: メモリ割り当て器が実際に使用中のメモリ量 + - field: total_system_memory + type: 0 + i18n: + zh-CN: 总系统内存容量(字节) + en-US: Total System Memory + ja-JP: 使用可能なシステムメモリの合計(バイト) + - field: total_system_memory_human + type: 0 + unit: GB + i18n: + zh-CN: 总系统内存容量 + en-US: Total System Memory Human + ja-JP: 使用可能なシステムメモリの合計 + - field: used_memory_lua + type: 0 + i18n: + zh-CN: LUA脚本占用的内存(字节) + en-US: Used Memory LUA + ja-JP: Luaスクリプトが使用中のメモリの量(バイト) + - field: used_memory_lua_human + type: 0 + unit: KB + i18n: + zh-CN: LUA脚本占用的内存 + en-US: Used Memory LUA Human + ja-JP: Luaスクリプトが使用しているメモリの量 + - field: used_memory_scripts + type: 0 + i18n: + zh-CN: Valkey缓存的LUA脚本大小(字节) + en-US: Used Memory Scripts + ja-JP: キャッシュされたLuaスクリプトのメモリ量(バイト) + - field: used_memory_scripts_human + type: 0 + unit: KB + i18n: + zh-CN: Valkey缓存的LUA脚本大小 + en-US: Used Memory Scripts Human + ja-JP: キャッシュされているLuaスクリプトのメモリ量 + - field: number_of_cached_scripts + type: 0 + i18n: + zh-CN: Valkey缓存的LUA脚本数量 + en-US: Number Of Cached Scripts + ja-JP: キャッシュされたLuaスクリプトの数 + - field: maxmemory + type: 0 + i18n: + zh-CN: 最大内存限制(字节) + en-US: Max Memory + ja-JP: メモリの最大容量(バイト) + - field: maxmemory_human + type: 0 + unit: MB + i18n: + zh-CN: 最大内存限制 + en-US: Max Memory Human + ja-JP: メモリの最大容量 + - field: maxmemory_policy + type: 1 + i18n: + zh-CN: 内存淘汰策略 + en-US: Max Memory Policy + ja-JP: キーの削除ポリシー + - field: allocator_frag_ratio + type: 0 + i18n: + zh-CN: 内存分配器中的内存碎片占比 + en-US: Allocator Frag Ratio + ja-JP: メモリ割り当て器のメモリ断片化の比率 + - field: allocator_frag_bytes + type: 0 + i18n: + zh-CN: 内存分配器中的内存碎片大小 + en-US: Allocator Frag Bytes + ja-JP: メモリ割り当て器のメモリ断片化のバイト数 + - field: allocator_rss_ratio + type: 0 + i18n: + zh-CN: 内存分配器分配的内存占比 + en-US: Allocator RSS Ratio + ja-JP: メモリ割り当て器の物理メモリの比率 + - field: allocator_rss_bytes + type: 0 + i18n: + zh-CN: 内存分配器分配的内存大小 + en-US: Allocator RSS Bytes + ja-JP: メモリ割り当て器の実際に使用中の物理メモリのバイト数 + - field: rss_overhead_ratio + type: 0 + i18n: + zh-CN: 实际内存占比 + en-US: RSS Overhead Ratio + ja-JP: 物理メモリオーバーヘッドの比率 + - field: rss_overhead_bytes + type: 0 + i18n: + zh-CN: 实际内存大小 + en-US: RSS Overhead Bytes + ja-JP: 物理メモリオーバーヘッドのバイト数 + - field: mem_fragmentation_ratio + type: 0 + i18n: + zh-CN: 内存碎片率 + en-US: Mem Fragmentation Ratio + ja-JP: メモリ断片化の比率 + - field: mem_fragmentation_bytes + type: 0 + i18n: + zh-CN: 内存碎片大小 + en-US: Mem Fragmentation Bytes + ja-JP: メモリ断片化によって発生したバイト数 + - field: mem_not_counted_for_evict + type: 0 + i18n: + zh-CN: 未计入最大内存限制的内存 + en-US: Mem Not Counted For Evict + ja-JP: 強制抽出の対象ではないメモリの量 + - field: mem_replication_backlog + type: 0 + i18n: + zh-CN: 主从同步缓冲区占用的内存(字节) + en-US: Mem Replication Backlog + ja-JP: 複製バックログが使用中のメモリの量 + - field: mem_clients_slaves + type: 0 + i18n: + zh-CN: 从节点占用的内存 + en-US: Mem Clients Slaves + ja-JP: スレーブクライアントが使用中のメモリの量 + - field: mem_clients_normal + type: 0 + i18n: + zh-CN: 客户端占用的内存 + en-US: Mem Clients Normal + ja-JP: クライアントが使用中のメモリの量 + - field: mem_aof_buffer + type: 0 + i18n: + zh-CN: AOF缓冲区占用的内存 + en-US: Mem AOF Buffer + ja-JP: AOFバッファで使用中のメモリ量 + - field: mem_allocator + type: 1 + i18n: + zh-CN: 内存分配器 + en-US: Mem Allocator + ja-JP: メモリ割り当て器 + - field: active_defrag_running + type: 0 + i18n: + zh-CN: 是否正在整理内存 + en-US: Active Defrag Running + ja-JP: アクティブなデフラグ操作のステータス + - field: lazyfree_pending_objects + type: 0 + i18n: + zh-CN: 待处理的惰性删除对象数量 + en-US: Lazyfree Pending Objects + ja-JP: Lazyfree作業が保留中のオブジェクトの数 + - field: lazyfreed_objects + type: 0 + i18n: + zh-CN: 已经执行的惰性删除对象数量 + en-US: Lazyfreed Objects + ja-JP: Lazyfre作業で解除されたオブジェクトの数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - persistence + - name: persistence + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + i18n: + zh-CN: 持久化信息 + en-US: Persistence + ja-JP: 永続化情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: loading + type: 0 + i18n: + zh-CN: 是否正在加载持久化文件 + en-US: Loading + ja-JP: データロード有無 + - field: current_cow_size + type: 0 + i18n: + zh-CN: COW区域内存大小 + en-US: Current COW Size + ja-JP: 現在COWのサイズ + - field: current_cow_size_age + type: 0 + i18n: + zh-CN: COW区域内存使用时间 + en-US: Current COW Size Age + ja-JP: 現在COWのサイズスパン + - field: current_fork_perc + type: 0 + i18n: + zh-CN: COW区域内存使用率 + en-US: Current Fork Perc + ja-JP: 現在のフォーク作業の進行率 + - field: current_save_keys_processed + type: 0 + i18n: + zh-CN: 正在处理的保存键数量 + en-US: Current Save Keys Processed + ja-JP: 現在の保存作業で処理されたキーの数 + - field: current_save_keys_total + type: 0 + i18n: + zh-CN: 保存键总数量 + en-US: Current Save Keys Total + ja-JP: 現在の保存作業で処理するキー数の合計 + - field: rdb_changes_since_last_save + type: 0 + i18n: + zh-CN: 自最近一次RDB后的数据改动条数 + en-US: RDB Changes Since Last Save + ja-JP: RDBファイルを最後に保存して以来、入力されたデータ件数 + - field: rdb_bgsave_in_progress + type: 0 + i18n: + zh-CN: 是否正在进行RDB的bgsave命令 + en-US: RDB bgsave In Progress + ja-JP: バックグラウンドRDBの保存作業が進行中 + - field: rdb_last_save_time + type: 0 + i18n: + zh-CN: 最近一次bgsave命令执行时间 + en-US: RDB Last Save Time + ja-JP: 最後のRDB保存時刻 + - field: rdb_last_bgsave_status + type: 1 + i18n: + zh-CN: 最近一次bgsave命令执行状态 + en-US: RDB Last bgsave Status + ja-JP: 最後のRDB保存ステータス + - field: rdb_last_bgsave_time_sec + type: 0 + i18n: + zh-CN: 最近一次bgsave命令执行时间(秒) + en-US: RDB Last bgsave Time Sec + ja-JP: 最後のバックグラウンドRDB保存にかかった時間(秒) + - field: rdb_current_bgsave_time_sec + type: 0 + i18n: + zh-CN: 当前bgsave命令执行时间(秒) + en-US: RDB Current bgsave Time Sec + ja-JP: 現在バックグラウンドでRDBを保存している場合、保存開始から現在までの所要時間(秒) + - field: rdb_last_cow_size + type: 0 + i18n: + zh-CN: RDB最近一次COW区域内存大小 + en-US: RDB Last COW Size + ja-JP: 最後のRDB保存時点のCOWのサイズ + - field: aof_enabled + type: 0 + i18n: + zh-CN: 是否开启了AOF + en-US: AOF Enabled + ja-JP: AOFの活性化状態の有無 + - field: aof_rewrite_in_progress + type: 0 + i18n: + zh-CN: 是否正在进行AOF的rewrite命令 + en-US: AOF rewrite In Progress + ja-JP: バックグラウンドAOFの保存作業が進行中 + - field: aof_rewrite_scheduled + type: 0 + i18n: + zh-CN: 是否在RDB的bgsave结束后执行AOF的rewirte + en-US: AOF rewrite Scheduled + ja-JP: AOF rewriteを実行する予定 + - field: aof_last_rewrite_time_sec + type: 0 + i18n: + zh-CN: 最近一次AOF的rewrite命令执行时间(秒) + en-US: AOF Last rewrite Time Sec + ja-JP: 最後のAOF rewriteにかかった時間(秒) + - field: aof_current_rewrite_time_sec + type: 0 + i18n: + zh-CN: 当前rewrite命令执行时间(秒) + en-US: AOF Current rewrite Time Sec + ja-JP: 現在AOF rewriteにかかった時間(秒) + - field: aof_last_bgrewrite_status + type: 1 + i18n: + zh-CN: 最近一次AOF的bgrewrite命令执行状态 + en-US: AOF Last bgrewrite Status + ja-JP: 最後のAOF rewriteのステータス + - field: aof_last_write_status + type: 1 + i18n: + zh-CN: 最近一次AOF写磁盘结果 + en-US: AOF Last Write Status + ja-JP: 最後のAOF書き込みのステータス + - field: aof_last_cow_size + type: 0 + i18n: + zh-CN: AOF最近一次COW区域内存大小 + en-US: AOF Last Cow Size + ja-JP: 最後のAOF rewrite時点のCOWのサイズ + - field: module_fork_in_progress + type: 0 + i18n: + zh-CN: 是否正在进行fork操作 + en-US: Module Fork In Progress + ja-JP: モジュールフォークの進行有無 + - field: module_fork_last_cow_size + type: 0 + i18n: + zh-CN: 最近一次执行fork操作的COW区域内存大小 + en-US: Module Fork Last Cow Size + ja-JP: 最後のモジュールフォーク時点のCOWのサイズ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - stats + - name: stats + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 4 + i18n: + zh-CN: 全局统计信息 + en-US: Stats + ja-JP: 統計情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: total_connections_received + type: 0 + i18n: + zh-CN: 已接受的总连接数 + en-US: Total Connections Received + ja-JP: サーバーの起動後に受信された合計接続数 + - field: total_commands_processed + type: 0 + i18n: + zh-CN: 执行过的命令总数 + en-US: Total Commands Processed + ja-JP: サーバーの起動後に処理した総コマンド数 + - field: instantaneous_ops_per_sec + type: 0 + i18n: + zh-CN: 命令处理条数/秒 + en-US: Instantaneous Ops Per Sec + ja-JP: 1秒当たりに処理されたコマンドの数 + - field: total_net_input_bytes + type: 0 + i18n: + zh-CN: 输入总网络流量(字节) + en-US: Total Net Input Bytes + ja-JP: 受信されたネットワークバイトの合計数 + - field: total_net_output_bytes + type: 0 + i18n: + zh-CN: 输出总网络流量(字节) + en-US: Total Net Output Bytes + ja-JP: 転送されたネットワークバイトの合計数 + - field: instantaneous_input_kbps + type: 0 + i18n: + zh-CN: 输入字节数/秒 + en-US: Instantaneous Input Kbps + ja-JP: 瞬間入力速度(kbps) + - field: instantaneous_output_kbps + type: 0 + i18n: + zh-CN: 输出字节数/秒 + en-US: Instantaneous Output Kbps + ja-JP: 瞬間出力速度(kbps) + - field: rejected_connections + type: 0 + i18n: + zh-CN: 拒绝连接数 + en-US: Rejected Connections + ja-JP: maxclients制限で拒否された接続数 + - field: sync_full + type: 0 + i18n: + zh-CN: 主从完全同步成功次数 + en-US: Sync Full + ja-JP: 完全同期の回数 + - field: sync_partial_ok + type: 0 + i18n: + zh-CN: 主从部分同步成功次数 + en-US: Sync Partial OK + ja-JP: 部分同期の回数 + - field: sync_partial_err + type: 0 + i18n: + zh-CN: 主从部分同步失败次数 + en-US: Sync Partial Error + ja-JP: 部分同期のエラー回数 + - field: expired_keys + type: 0 + i18n: + zh-CN: 过期key数量 + en-US: Expired Keys + ja-JP: expireコマンドで削除されたキーの数 + - field: expired_stale_perc + type: 0 + i18n: + zh-CN: 过期key占比 + en-US: Expired Stale Perc + ja-JP: 期限切れのステイルキーの比率 + - field: expired_time_cap_reached_count + type: 0 + i18n: + zh-CN: 过期key清理操作被限制次数 + en-US: Expired Time Cap Reached Count + ja-JP: 満了時間の制限に達した回数 + - field: expire_cycle_cpu_milliseconds + type: 0 + i18n: + zh-CN: 清理过期key消耗的cpu时间(毫秒) + en-US: Expire Cycle CPU Milliseconds + ja-JP: 満了サイクルでかかったCPU時間(ms) + - field: evicted_keys + type: 0 + i18n: + zh-CN: 淘汰key数量 + en-US: Evicted Keys + ja-JP: メモリ不足で追放されたキーの数 + - field: keyspace_hits + type: 0 + i18n: + zh-CN: key命中成功次数 + en-US: Keyspace Hits + ja-JP: キースペースのヒット数 + - field: keyspace_misses + type: 0 + i18n: + zh-CN: key命中失败次数 + en-US: Keyspace Misses + ja-JP: キースペースのミス数 + - field: pubsub_channels + type: 0 + i18n: + zh-CN: 订阅的频道数量 + en-US: Pubsub Channels + ja-JP: 有効になっているPub/Subチャネルに接続されているチャネルの数 + - field: pubsub_patterns + type: 0 + i18n: + zh-CN: 订阅的模式数量 + en-US: Pubsub Patterns + ja-JP: 有効になっているPub/Subパターンで接続されたパターンチャネルの数 + - field: latest_fork_usec + type: 0 + i18n: + zh-CN: 最近一次fork操作消耗时间(微秒) + en-US: Latest Fork Usec + ja-JP: 最後のフォーク作業にかかった時間 + - field: total_forks + type: 0 + i18n: + zh-CN: fork进程总数 + en-US: Total Forks + ja-JP: 実行されたフォーク作業の合計数 + - field: migrate_cached_sockets + type: 0 + i18n: + zh-CN: 正在进行migrate的Socket个数 + en-US: Migrate Cached Sockets + ja-JP: マイグレーションされたソケット数 + - field: slave_expires_tracked_keys + type: 0 + i18n: + zh-CN: 主从同步中已过期的key数量 + en-US: Slave Expires Tracked Keys + ja-JP: スレーブで満了した、トラッキングされたキーの数 + - field: active_defrag_hits + type: 0 + i18n: + zh-CN: 主动碎片整理命中次数 + en-US: Active Defrag Hits + ja-JP: アクティブなデフラグ操作中に発生したヒットの数 + - field: active_defrag_misses + type: 0 + i18n: + zh-CN: 主动碎片整理未命中次数 + en-US: Active Defrag Misses + ja-JP: アクティブなデフラグ操作中に見逃したヒットの数 + - field: active_defrag_key_hits + type: 0 + i18n: + zh-CN: 主动碎片整理key命中次数 + en-US: Active Defrag Key Hits + ja-JP: デフラグ操作でヒットしたキーの数 + - field: active_defrag_key_misses + type: 0 + i18n: + zh-CN: 主动碎片整理key未命中次数 + en-US: Active Defrag Key Misses + ja-JP: デフラグ操作で見逃したキーの数 + - field: tracking_total_keys + type: 0 + i18n: + zh-CN: 正在追踪的key数量 + en-US: Tracking Total Keys + ja-JP: トラッキングされたキーの合計数 + - field: tracking_total_items + type: 0 + i18n: + zh-CN: 正在追踪的value数量 + en-US: Tracking Total Items + ja-JP: トラッキングされたアイテムの合計数 + - field: tracking_total_prefixes + type: 0 + i18n: + zh-CN: 正在追踪的前缀数量 + en-US: Tracking Total Prefixes + ja-JP: トラッキングされたプレフィックスの合計数 + - field: unexpected_error_replies + type: 0 + i18n: + zh-CN: 执行命令时发生错误的数量 + en-US: Unexpected Error Replies + ja-JP: 予期しないエラー応答の数 + - field: total_error_replies + type: 0 + i18n: + zh-CN: 执行命令时发生错误的总数 + en-US: Total Error Replies + ja-JP: 発生したエラーの合計応答の数 + - field: dump_payload_sanitizations + type: 0 + i18n: + zh-CN: Dump命令时数据清理的数量 + en-US: Dump Payload Sanitizations + ja-JP: ダンプペイロードで実行された整理作業の数 + - field: total_reads_processed + type: 0 + i18n: + zh-CN: 执行读操作时处理的请求数量 + en-US: Total Reads Processed + ja-JP: 処理された読み取り作業の合計数 + - field: total_writes_processed + type: 0 + i18n: + zh-CN: 执行写操作时处理的请求数量 + en-US: Total Writes Processed + ja-JP: 処理された書き込み作業の合計数 + - field: io_threaded_reads_processed + type: 0 + i18n: + zh-CN: 使用线程进行读操作时处理的请求数量 + en-US: IO Threaded Reads Processed + ja-JP: I/Oスレッドで処理された読み取り作業の数 + - field: io_threaded_writes_processed + type: 0 + i18n: + zh-CN: 使用线程进行写操作时处理的请求数量 + en-US: IO Threaded Writes Processed + ja-JP: I/Oスレッドで処理された書き込み作業の数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - replication + - name: replication + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 5 + i18n: + zh-CN: 主从同步信息 + en-US: Replication + ja-JP: レプリケーション情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: role + type: 1 + i18n: + zh-CN: 节点角色 + en-US: Role + ja-JP: ノード役割 + - field: connected_slaves + type: 0 + i18n: + zh-CN: 已连接的从节点个数 + en-US: Connected Slaves + ja-JP: 接続されたスレーブの数 + - field: master_failover_state + type: 1 + i18n: + zh-CN: 主从故障转移状态 + en-US: Master FailOver State + ja-JP: マスターノードのフェイルオーバー処理の状態 + - field: master_replid + type: 1 + i18n: + zh-CN: 主从同步id + en-US: Master Replid + ja-JP: マスターノードのレプリケーションID + - field: master_replid2 + type: 0 + i18n: + zh-CN: 主从同步id2 + en-US: Master Replid2 + ja-JP: マスターノードのレプリケーションID2 + - field: master_repl_offset + type: 0 + i18n: + zh-CN: 主节点偏移量 + en-US: Master Repl Offset + ja-JP: マスターノードの複製におけるオフセット値 + - field: second_repl_offset + type: 0 + i18n: + zh-CN: 接受主从同步的从节点偏移量 + en-US: Second Repl Offset + ja-JP: マスタとセカンダリ間の複製におけるオフセット値 + - field: repl_backlog_active + type: 0 + i18n: + zh-CN: 复制缓冲区状态 + en-US: Repl Backlog Active + ja-JP: 複製バックログの状態 + - field: repl_backlog_size + type: 0 + i18n: + zh-CN: 复制缓冲区大小(字节) + en-US: Repl Backlog Size + ja-JP: 複製バックログのバイト数 + - field: repl_backlog_first_byte_offset + type: 0 + i18n: + zh-CN: 复制缓冲区起始偏移量 + en-US: Repl Backlog First Byte Offset + ja-JP: 複製バックログにおける最初のバイトのオフセット + - field: repl_backlog_histlen + type: 0 + i18n: + zh-CN: 复制缓冲区的有效数据长度 + en-US: Repl Backlog Histlen + ja-JP: 複製バックログの履歴の長さ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - cpu + - name: cpu + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 6 + i18n: + zh-CN: CPU消耗信息 + en-US: CPU + ja-JP: CPU情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: used_cpu_sys + type: 0 + i18n: + zh-CN: Valkey进程使用的CPU时钟总和(内核态) + en-US: Used CPU Sys + ja-JP: システムモードで使用されたCPUの合計時間 + - field: used_cpu_user + type: 0 + i18n: + zh-CN: Valkey进程使用的CPU时钟总和(用户态) + en-US: Used CPU User + ja-JP: ユーザーモードで使用されたCPUの合計時間 + - field: used_cpu_sys_children + type: 0 + i18n: + zh-CN: 后台进程使用的CPU时钟总和(内核态) + en-US: Used CPU Sys Children + ja-JP: 子プロセスがシステムモードで使用したCPU時間 + - field: used_cpu_user_children + type: 0 + i18n: + zh-CN: 后台进程使用的CPU时钟总和(用户态) + en-US: Used CPU User Children + ja-JP: 子プロセスがユーザーモードで使用したCPU時間 + - field: used_cpu_sys_main_thread + type: 0 + i18n: + zh-CN: 主进程使用的CPU时钟总和(内核态) + en-US: Used CPU Sys Main Thread + ja-JP: プロセスがシステムモードで使用したCPU時間 + - field: used_cpu_user_main_thread + type: 0 + i18n: + zh-CN: 主进程使用的CPU时钟总和(用户态) + en-US: Used CPU User Main Thread + ja-JP: プロセスがユーザーモードで使用したCPU時間 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - errorstats + - name: errorstats + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 8 + i18n: + zh-CN: 错误信息 + en-US: Error Stats + ja-JP: エラー統計情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: errorstat_ERR + type: 1 + i18n: + zh-CN: 执行命令时出错的次数 + en-US: Error Stat Error + ja-JP: コマンド実行期間のエラー発生数 + - field: errorstat_MISCONF + type: 1 + i18n: + zh-CN: 执行命令时出现misconf错误的次数 + en-US: Error Stat Misconf + ja-JP: コマンド実行期間のmisconfエラー発生数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - cluster + - name: cluster + # collect metrics content + priority: 9 + i18n: + zh-CN: 集群信息 + en-US: Cluster + ja-JP: クラスター情報 + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: cluster_enabled + type: 0 + i18n: + zh-CN: 节点是否开启集群模式 + en-US: Cluster Enabled + ja-JP: クラスターの有効化 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - commandstats + - name: commandstats + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 9 + i18n: + zh-CN: 命令统计信息 + en-US: Command Stats + ja-JP: コマンド統計情報 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: cmdstat_set + type: 1 + i18n: + zh-CN: SET 命令统计 + en-US: SET COMMAND STAT + ja-JP: SET コマンド統計 + - field: cmdstat_get + type: 1 + i18n: + zh-CN: GET 命令统计 + en-US: GET COMMAND STAT + ja-JP: GET コマンド統計 + - field: cmdstat_setnx + type: 1 + i18n: + zh-CN: SETNX 命令统计 + en-US: SETNX COMMAND STAT + ja-JP: SETNX コマンド統計 + - field: cmdstat_hset + type: 1 + i18n: + zh-CN: HSET 命令统计 + en-US: HSET COMMAND STAT + ja-JP: HSET コマンド統計 + - field: cmdstat_hget + type: 1 + i18n: + zh-CN: HGET 命令统计 + en-US: HGET COMMAND STAT + ja-JP: HGET コマンド統計 + - field: cmdstat_lpush + type: 1 + i18n: + zh-CN: LPUSH 命令统计 + en-US: LPUSH COMMAND STAT + ja-JP: LPUSH コマンド統計 + - field: cmdstat_rpush + type: 1 + i18n: + zh-CN: RPUSH 命令统计 + en-US: RPUSH COMMAND STAT + ja-JP: RPUSH コマンド統計 + - field: cmdstat_lpop + type: 1 + i18n: + zh-CN: LPOP 命令统计 + en-US: LPOP COMMAND STAT + ja-JP: LPOP コマンド統計 + - field: cmdstat_rpop + type: 1 + i18n: + zh-CN: RPOP 命令统计 + en-US: RPOP COMMAND STAT + ja-JP: RPOP コマンド統計 + - field: cmdstat_llen + type: 1 + i18n: + zh-CN: LLEN 命令统计 + en-US: LLEN COMMAND STAT + ja-JP: LLEN コマンド統計 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + + # metrics - keyspace + - name: keyspace + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 9 + i18n: + zh-CN: 键空间 + en-US: Keyspace + ja-JP: キー空間 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: db0 + type: 1 + i18n: + zh-CN: db0 + en-US: db0 + ja-JP: db0 + - field: db1 + type: 1 + i18n: + zh-CN: db1 + en-US: db1 + ja-JP: db1 + - field: db2 + type: 1 + i18n: + zh-CN: db2 + en-US: db2 + ja-JP: db2 + - field: db3 + type: 1 + i18n: + zh-CN: db3 + en-US: db3 + ja-JP: db3 + - field: db4 + type: 1 + i18n: + zh-CN: db4 + en-US: db4 + ja-JP: db4 + - field: db5 + type: 1 + i18n: + zh-CN: db5 + en-US: db5 + ja-JP: db5 + - field: db6 + type: 1 + i18n: + zh-CN: db6 + en-US: db6 + ja-JP: db6 + - field: db7 + type: 1 + i18n: + zh-CN: db7 + en-US: db7 + ja-JP: db7 + - field: db8 + type: 1 + i18n: + zh-CN: db8 + en-US: db8 + ja-JP: db8 + - field: db9 + type: 1 + i18n: + zh-CN: db9 + en-US: db9 + ja-JP: db9 + - field: db10 + type: 1 + i18n: + zh-CN: db10 + en-US: db10 + ja-JP: db10 + - field: db11 + type: 1 + i18n: + zh-CN: db11 + en-US: db11 + ja-JP: db11 + - field: db12 + type: 1 + i18n: + zh-CN: db12 + en-US: db12 + ja-JP: db12 + - field: db13 + type: 1 + i18n: + zh-CN: db13 + en-US: db13 + ja-JP: db13 + - field: db14 + type: 1 + i18n: + zh-CN: db14 + en-US: db14 + ja-JP: db14 + - field: db15 + type: 1 + i18n: + zh-CN: db15 + en-US: db15 + ja-JP: db15 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: redis + # the config content when protocol is redis + redis: + # redis host: ipv4 ipv6 host + host: ^_^host^_^ + # redis port + port: ^_^port^_^ + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-vastbase.yml b/hertzbeat-manager/src/main/resources/define/app-vastbase.yml new file mode 100644 index 0000000..8676d83 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-vastbase.yml @@ -0,0 +1,837 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +# The monitoring type eg: linux windows tomcat mysql aws... +app: vastbase +# The monitoring i18n name +name: + zh-CN: Vastbase数据库 + en-US: Vastbase DB + ja-JP: Vastbase データベース +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 Vastbase 数据库的通用性能指标 (basic、state、activity etc) 进行采集监控,支持版本为 Vastbase 9.2.4+。<br>您可以点击“<i>新建 Vastbase 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC Protocol</a> to configure SQL for collecting general metrics of Vastbase database (basic、state、activity etc). Supported version is Vastbase 9.2.4+. <br>You can click "<i>New Vastbase Database</i>" and configure it, or select "<i>More Action</i>" to import the existing configuration. + zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 協議</a> 通過配置 SQL 對 Vastbase 數據庫的通用性能指標 (basic、state、activity etc)進行采集監控,支持版本爲 Vastbase 9.2.4+。<br>您可以點擊“<i>新建 Vastbase 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBCプロトコルを介して</a> Vastbase データベース(Vastbase 9.2.4+)の一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Vastbase データベース</i>」をクリックしてパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/vastbase + en-US: https://hertzbeat.apache.org/docs/help/vastbase +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 5432 + - field: timeout + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + type: number + range: '[400,200000]' + required: false + hide: true + defaultValue: 6000 + - field: database + name: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + type: text + defaultValue: postgres + required: false + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + - field: url + name: + zh-CN: URL + en-US: URL + ja-JP: URL + type: text + required: false + hide: true + +# collect metrics config list +metrics: + # metrics - basic + - name: basic + i18n: + zh-CN: 基本信息 + en-US: Basic Info + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: server_version + type: 1 + label: true + i18n: + zh-CN: 服务器版本 + en-US: Server Version + ja-JP: バージョン + - field: port + type: 1 + i18n: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + - field: server_encoding + type: 1 + i18n: + zh-CN: 服务器编码 + en-US: Server Encoding + ja-JP: サーバーのエンコード + - field: data_directory + type: 1 + i18n: + zh-CN: 数据目录 + en-US: Data Directory + ja-JP: データディレクトリ + - field: max_connections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # host: ipv4 ipv6 host + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + # database platform name + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + # sql + sql: select name, setting as value from pg_settings where name = 'max_connections' or name = 'server_version' or name = 'server_encoding' or name = 'port' or name = 'data_directory'; + # JDBC url + url: ^_^url^_^ + + - name: state + i18n: + zh-CN: 状态信息 + en-US: State Info + ja-JP: 状態情報 + priority: 1 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: conflicts + type: 0 + unit: times + i18n: + zh-CN: 冲突次数 + en-US: Conflicts + ja-JP: コンフリクト回数 + - field: deadlocks + type: 0 + unit: times + i18n: + zh-CN: 死锁次数 + en-US: Deadlocks + ja-JP: デッドロック回数 + - field: blks_read + type: 0 + unit: blocks per second + i18n: + zh-CN: 读取块 + en-US: Blocks Read + ja-JP: 読み取られたブロック + - field: blks_hit + type: 0 + unit: blocks per second + i18n: + zh-CN: 命中块 + en-US: Blocks Hit + ja-JP: ヒットブロック + - field: blk_read_time + type: 0 + unit: ms + i18n: + zh-CN: 读取时间 + en-US: Read Time + ja-JP: 読み取られタイム + - field: blk_write_time + type: 0 + unit: ms + i18n: + zh-CN: 写入时间 + en-US: Write Time + ja-JP: 書き込まれタイム + - field: stats_reset + type: 1 + i18n: + zh-CN: 统计重置 + en-US: Stats Reset + ja-JP: 統計リセット + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: SELECT COALESCE(datname,'shared-object') as db_name, conflicts, deadlocks, blks_read, blks_hit, blk_read_time, blk_write_time, stats_reset from pg_stat_database where (datname != 'template1' and datname != 'template0') or datname is null; + url: ^_^url^_^ + + - name: activity + i18n: + zh-CN: 活动信息 + en-US: Activity Info + ja-JP: 活動情報 + priority: 2 + fields: + - field: running + type: 0 + unit: sbc + i18n: + zh-CN: 运行中 + en-US: Running + ja-JP: 実行中 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: SELECT count(*) as running FROM pg_stat_activity WHERE NOT pid=pg_backend_pid(); + url: ^_^url^_^ + + - name: resource_config + i18n: + zh-CN: 资源配置 + en-US: Resource Config + ja-JP: リソース設定 + priority: 3 + fields: + - field: work_mem + type: 0 + unit: MB + i18n: + zh-CN: 工作内存 + en-US: Work Memory + ja-JP: ワークメモリ + - field: shared_buffers + type: 0 + unit: MB + i18n: + zh-CN: 共享缓冲区 + en-US: Shared Buffers + ja-JP: 共有バッファ + - field: autovacuum + type: 1 + i18n: + zh-CN: 自动清理 + en-US: Auto Vacuum + ja-JP: オートバキューム + - field: max_connections + type: 0 + i18n: + zh-CN: 最大连接数 + en-US: Max Connections + ja-JP: 最大接続数 + - field: effective_cache_size + type: 0 + unit: MB + i18n: + zh-CN: 有效缓存大小 + en-US: Effective Cache Size + ja-JP: キャッシュサイズ + - field: wal_buffers + type: 0 + unit: MB + i18n: + zh-CN: WAL缓冲区 + en-US: WAL Buffers + ja-JP: WALバッファ + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: columns + sql: show all; + url: ^_^url^_^ + + - name: connection + i18n: + zh-CN: 连接信息 + en-US: Connection Info + ja-JP: 接続情報 + priority: 4 + fields: + - field: active + type: 0 + i18n: + zh-CN: 活动连接 + en-US: Active Connection + ja-JP: 活躍的な接続 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: select count(1) as active from pg_stat_activity; + url: ^_^url^_^ + + - name: connection_state + i18n: + zh-CN: 连接状态 + en-US: Connection State + ja-JP: 接続状態 + priority: 5 + fields: + - field: state + type: 1 + label: true + i18n: + zh-CN: 状态 + en-US: State + ja-JP: 状態 + - field: num + type: 0 + i18n: + zh-CN: 数量 + en-US: Num + ja-JP: 数量 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select COALESCE(state, 'other') as state, count(*) as num from pg_stat_activity group by state; + url: ^_^url^_^ + + - name: connection_db + i18n: + zh-CN: 连接数据库 + en-US: Connection Db + ja-JP: 接続データベース + priority: 6 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: active + type: 0 + i18n: + zh-CN: 活动连接 + en-US: Active Connection + ja-JP: 活躍的な接続 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select count(*) as active, COALESCE(datname, 'other') as db_name from pg_stat_activity group by datname; + url: ^_^url^_^ + + - name: tuple + i18n: + zh-CN: 元组信息 + en-US: Tuple Info + ja-JP: 組情報 + priority: 7 + fields: + - field: fetched + type: 0 + i18n: + zh-CN: 获取次数 + en-US: Fetched + ja-JP: フェッチ回数 + - field: returned + type: 0 + i18n: + zh-CN: 返回次数 + en-US: Returned + ja-JP: 戻る回数 + - field: inserted + type: 0 + i18n: + zh-CN: 插入次数 + en-US: Inserted + ja-JP: インサート回数 + - field: updated + type: 0 + i18n: + zh-CN: 更新次数 + en-US: Updated + ja-JP: 更新回数 + - field: deleted + type: 0 + i18n: + zh-CN: 删除次数 + en-US: Deleted + ja-JP: 削除回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select sum(tup_fetched) as fetched, sum(tup_updated) as updated, sum(tup_deleted) as deleted, sum(tup_inserted) as inserted, sum(tup_returned) as returned from pg_stat_database; + url: ^_^url^_^ + + - name: temp_file + i18n: + zh-CN: 临时文件 + en-US: Temp File + ja-JP: 一時ファイル + priority: 8 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: num + type: 0 + i18n: + zh-CN: 次数 + en-US: Num + ja-JP: 数量 + - field: Size + type: 0 + unit: B + i18n: + zh-CN: 大小 + en-US: Size + ja-JP: サイズ + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select COALESCE(datname, 'other') as db_name, sum(temp_files) as num, sum(temp_bytes) as size from pg_stat_database group by datname; + url: ^_^url^_^ + + - name: lock + i18n: + zh-CN: 锁信息 + en-US: Lock Info + ja-JP: ロック情報 + priority: 9 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: conflicts + type: 0 + unit: times + i18n: + zh-CN: 冲突次数 + en-US: Conflicts + ja-JP: コンフリクト回数 + - field: deadlocks + type: 0 + unit: times + i18n: + zh-CN: 死锁次数 + en-US: Deadlocks + ja-JP: デッドロック回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: SELECT COALESCE(datname,'shared-object') as db_name, conflicts, deadlocks from pg_stat_database where (datname != 'template1' and datname != 'template0') or datname is null; + url: ^_^url^_^ + + - name: slow_sql + i18n: + zh-CN: 慢查询 + en-US: Slow Sql + ja-JP: スローSQL + priority: 10 + fields: + - field: sql_text + type: 1 + label: true + i18n: + zh-CN: SQL语句 + en-US: SQL Text + ja-JP: SQL文のテキスト + - field: calls + type: 0 + i18n: + zh-CN: 调用次数 + en-US: Calls + ja-JP: コール回数 + - field: rows + type: 0 + i18n: + zh-CN: 行数 + en-US: Rows + ja-JP: 行 + - field: avg_time + type: 0 + unit: ms + i18n: + zh-CN: 平均时间 + en-US: Avg Time + ja-JP: 平均時間 + - field: total_time + type: 0 + unit: ms + i18n: + zh-CN: 总时间 + en-US: Total Time + ja-JP: 合計時間 + aliasFields: + - query + - calls + - rows + - total_exec_time + - mean_exec_time + calculates: + - sql_text=query + - avg_time=mean_exec_time + - total_time=total_exec_time + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select * from pg_stat_statements; + url: ^_^url^_^ + + - name: transaction + i18n: + zh-CN: 事务信息 + en-US: Transaction Info + ja-JP: トランザクション情報 + priority: 11 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: commits + type: 0 + unit: times + i18n: + zh-CN: 提交次数 + en-US: Commits + ja-JP: コミット回数 + - field: rollbacks + type: 0 + unit: times + i18n: + zh-CN: 回滚次数 + en-US: Rollbacks + ja-JP: ロールバック回数 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select COALESCE(datname, 'other') as db_name, sum(xact_commit) as commits, sum(xact_rollback) as rollbacks from pg_stat_database group by datname; + url: ^_^url^_^ + + - name: conflicts + i18n: + zh-CN: 冲突信息 + en-US: Conflicts Info + ja-JP: コンフリクト情報 + priority: 12 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: tablespace + type: 0 + i18n: + zh-CN: 表空间 + en-US: Tablespace + ja-JP: 表領域 + - field: lock + type: 0 + i18n: + zh-CN: 锁 + en-US: Lock + ja-JP: ロック + - field: snapshot + type: 0 + i18n: + zh-CN: 快照 + en-US: Snapshot + ja-JP: スナップショット + - field: bufferpin + type: 0 + i18n: + zh-CN: 缓冲区 + en-US: Bufferpin + ja-JP: バッファ + - field: deadlock + type: 0 + i18n: + zh-CN: 死锁 + en-US: Deadlock + ja-JP: デッドロック + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select datname as db_name, confl_tablespace as tablespace, confl_lock as lock, confl_snapshot as snapshot, confl_bufferpin as bufferpin, confl_deadlock as deadlock from pg_stat_database_conflicts; + url: ^_^url^_^ + + - name: cache_hit_ratio + i18n: + zh-CN: 缓存命中率 + en-US: Cache Hit Ratio + ja-JP: キャッシュ命中率 + priority: 13 + fields: + - field: db_name + type: 1 + label: true + i18n: + zh-CN: 数据库名称 + en-US: Database Name + ja-JP: データベース名 + - field: ratio + type: 0 + unit: '%' + i18n: + zh-CN: 命中率 + en-US: Hit Ratio + ja-JP: 命中率 + aliasFields: + - blks_hit + - blks_read + - db_name + calculates: + - ratio=(blks_hit + 1) / (blks_read + blks_hit + 1) * 100 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: multiRow + sql: select datname as db_name, blks_hit, blks_read from pg_stat_database; + url: ^_^url^_^ + + - name: checkpoint + i18n: + zh-CN: Checkpoint信息 + en-US: Checkpoint Info + ja-JP: チェックポイント情報 + priority: 14 + fields: + - field: checkpoint_sync_time + type: 0 + unit: ms + i18n: + zh-CN: Checkpoint同步时间 + en-US: Checkpoint Sync Time + ja-JP: チェックポイント同期時間 + - field: checkpoint_write_time + type: 0 + unit: ms + i18n: + zh-CN: Checkpoint写入时间 + en-US: Checkpoint Write Time + ja-JP: Checkpoint書き込まれた時間 + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: select checkpoint_sync_time, checkpoint_write_time from pg_stat_bgwriter; + url: ^_^url^_^ + + - name: buffer + i18n: + zh-CN: Buffer信息 + en-US: Buffer Info + ja-JP: バッファ情報 + priority: 15 + fields: + - field: allocated + type: 0 + i18n: + zh-CN: 已分配 + en-US: Allocated + ja-JP: 割り当てバッファ + - field: fsync_calls_by_backend + type: 0 + i18n: + zh-CN: 后端进程直接执行的文件同步调用次数 + en-US: Fsync Calls By Backend + ja-JP: バックエンド同期コール回数 + - field: written_directly_by_backend + type: 0 + i18n: + zh-CN: 后台写入到数据文件 + en-US: Written Directly By Backend + ja-JP: バックエンドによる直接書き込まれたファイル + - field: written_by_background_writer + type: 0 + i18n: + zh-CN: 后台写入 + en-US: Written By Background Writer + ja-JP: バックグラウンドライターに書き込まれた + - field: written_during_checkpoints + type: 0 + i18n: + zh-CN: 检查点期间写入 + en-US: Written During Checkpoints + ja-JP: チェックポイント中の書き込み + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + platform: postgresql + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + queryType: oneRow + sql: select buffers_alloc as allocated, buffers_backend_fsync as fsync_calls_by_backend, buffers_backend as written_directly_by_backend, buffers_clean as written_by_background_writer, buffers_checkpoint as written_during_checkpoints from pg_stat_bgwriter; + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-website.yml b/hertzbeat-manager/src/main/resources/define/app-website.yml new file mode 100644 index 0000000..2834e69 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-website.yml @@ -0,0 +1,261 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: website +# The monitoring i18n name +name: + zh-CN: 网站监测 + en-US: WEBSITE + ja-JP: WEBSITE +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 支持对网站的是否可用,响应时间等指标进行采集监控。<br>您可以点击 “<i>新建 网站监测</i>” 并配置网站域名或IP进行添加,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat supports the collection and monitoring of website availability, response time and other metrics. <br>You can click "<i>New Website</i>" and configure the website domain name or IP to add it, or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 支持對網站的是否可用,響應時間等指標進行采集監控。<br>您可以點擊 “<i>新建 網站監測</i>” 並配置網站域名或IP進行添加,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeatは、WEBSITEが利用可能かどうかを確認し、応答時間(ms)などのメトリクスを監視します。「新規 WEBSITE」をクリックして設定しましょう。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/website + en-US: https://hertzbeat.apache.org/docs/help/websites +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 80 + # field-param field key + - field: uri + # name-param field display i18n name + name: + zh-CN: 相对路径 + en-US: URI + ja-JP: URI + # type-param field type(most mapping the html input tag) + type: text + # when type is text, use limit to limit string length + limit: 200 + # required-true or false + required: false + # param field input placeholder + placeholder: 'Website uri path(no ip port) EG:/console' + # field-param field key + - field: ssl + # name-param field display i18n name + name: + zh-CN: 启用HTTPS + en-US: HTTPS + ja-JP: HTTPS + # type-param field type(most mapping the html input type) + type: boolean + # required-true or false + required: true + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input tag) + type: number + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: authType + # name-param field display i18n name + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: false + # hide param-true or false + hide: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + # type-param field type(key-value mapping the html key-value input tags) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + # type-param field type(radio mapping the html radio tag) + type: password + # required-true or false + required: false + # hide param-true or false + hide: true + # field-param field key + - field: keyword + # name-param field display i18n name + name: + zh-CN: 计数关键字 + en-US: Keyword + ja-JP: キーワード + # type-param field type(key-value mapping the html key-value input tags) + type: text + # param field input placeholder + placeholder: 'Enter keyword which occurrences need to be monitored' + # required-true or false + required: false + # hide param-true or false + hide: true +metrics: + # metrics - summary, inner monitoring metrics (responseTime - response time, keyword - number of keywords) + - name: summary + i18n: + zh-CN: 概要 + en-US: Summary + ja-JP: 概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: keyword + type: 0 + i18n: + zh-CN: 关键词数量 + en-US: Keyword + ja-JP: キーワード + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^uri^_^ + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: website + keyword: ^_^keyword^_^ + - name: header + i18n: + zh-CN: 请求头 + en-US: Header + ja-JP: ヘッダ + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: content_type + type: 1 + i18n: + zh-CN: Content Type + en-US: Content Type + ja-JP: コンテンツタイプ + - field: content_length + type: 1 + i18n: + zh-CN: 响应内容长度 + en-US: Content Length + ja-JP: コンテンツの長さ + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: http + # the config content when protocol is http + http: + # http host: ipv4 ipv6 domain + host: ^_^host^_^ + # http port + port: ^_^port^_^ + # http url + url: ^_^uri^_^ + timeout: ^_^timeout^_^ + # http method: GET POST PUT DELETE PATCH + method: GET + # if enabled https + ssl: ^_^ssl^_^ + authorization: + # http auth type: Basic Auth, Digest Auth, Bearer Token + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ + # http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule + parseType: website + keyword: ^_^keyword^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-websocket.yml b/hertzbeat-manager/src/main/resources/define/app-websocket.yml new file mode 100644 index 0000000..9f6c8e2 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-websocket.yml @@ -0,0 +1,128 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: service +# The monitoring type eg: linux windows tomcat mysql aws... +app: websocket +# The monitoring i18n name +name: + zh-CN: WebSocket + en-US: WebSocket + ja-JP: WebSocket +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 对 WebSocket 服务的首次握手的响应等相关指标进行监测。<br>您可以点击 “<i>新建 WebSocket监控</i>” 并配置路径进行添加,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat monitors metrics such as the response of the WebSocket service's first handshake. <br>You can click "<i>New WebSocket</i>" and configure the uri to add it, or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: HertzBeat 對 WebSocket 服務的首次握手的響應等相關指標進行監測。<br>您可以點擊 “<i>新建 WebSocket監控</i>” 並配置路徑進行添加,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は WebSocketサービスの一般的なメトリクスを監視します。<br>「<i>新規 WebSocket</i>」をクリックしてパラメタを設定した後、新規することができます。 +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: WebSocket服务的Host + en-US: Host of WebSocket service + ja-JP: WebSocketサービスのホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # field-param field key + - field: path + # name-param field display i18n name + name: + zh-CN: WebSocket服务的路径 + en-US: Path of WebSocket service + ja-JP: WebSocketサービスのパス + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true + defaultValue: / +# collect metrics config list +metrics: + # metrics - summary + - name: summary + i18n: + zh-CN: 概要 + en-US: Summary + ja-JP: 概要 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + fields: + - field: responseTime + type: 0 + unit: ms + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + - field: httpVersion + type: 1 + i18n: + zh-CN: Http 版本 + en-US: Http Version + ja-JP: Httpバージョン + - field: responseCode + type: 1 + i18n: + zh-CN: 响应状态码 + en-US: Response Code + ja-JP: 応答コード + - field: statusMessage + type: 1 + i18n: + zh-CN: 状态消息 + en-US: Status Message + ja-JP: 状態メッセージ + - field: connection + type: 1 + i18n: + # Whether to support the upgrade (If no, the http request is normal; if yes, the upgrade is supported) + zh-CN: 是否升级 + en-US: Connection + ja-JP: 接続 + - field: upgrade + type: 1 + i18n: + zh-CN: 升级(协议) + en-US: Upgrade + ja-JP: アップグレード + + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: websocket + # Specific collection configuration when protocol is telnet protocol + websocket: + # telnet host + host: ^_^host^_^ + port: ^_^port^_^ + path: ^_^path^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-windows.yml b/hertzbeat-manager/src/main/resources/define/app-windows.yml new file mode 100644 index 0000000..35a7d2f --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-windows.yml @@ -0,0 +1,657 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: windows +# The monitoring i18n name +name: + zh-CN: Windows操作系统 + en-US: OS Windows + ja-JP: Windowsオーエス +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 协议</a> 对 Windows操作系统 的通用指标(用户数、进程数量、响应时间等)进行采集监控。<br><span class='help_module_span'>注意⚠️:Windows 服务器需要开启 SNMP 服务,<a class='help_module_content' href='https://learn.microsoft.com/zh-cn/troubleshoot/windows-server/networking/configure-snmp-service'>点击查看开启步骤</a>。</span>您可以点击 “<i>新建 Windows操作系统</i>” 并进行配置SNMP相关参数添加,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP Protocol</a> to monitoring OS Windows general performance metrics such as number of users, number of processes, response time. <br><span class='help_module_span'>Note⚠️:Windows server need enable the SNMP services <a class='help_module_content' href='https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/configure-snmp-service'>Click here to view the specific steps.</a></span> You can click the "<i>New OS Windows</i>" button and config snmp params to add monitor or import an existing setup through the "<i>More Actions</i>" menu. + zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 协议</a> 对 Windows操作系统 的通用指标(用户数、进程数量、响应时间等)进行采集监控。<br><span class='help_module_span'>注意⚠️:Windows 服务器需要开启 SNMP 服务,<a class='help_module_content' href='https://learn.microsoft.com/zh-cn/troubleshoot/windows-server/networking/configure-snmp-service'>点击查看开启步骤</a>。</span>您可以点击 “<i>新建 Windows操作系统</i>” 并进行配置SNMP相关参数添加,或者选择“<i>更多操作</i>”,导入已有配置。 + ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP プロトコルを介して</a> Windowsオーエスの一般的なメトリクスを監視します。<br><span class='help_module_span'>注意⚠️:Windowsサーバー で SNMP サービスを有効にする必要があります,<a class='help_module_content' href='https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/configure-snmp-service'>クリックしてガイドを見ます</a>。</span><br>「<i>新規 Windowsオーエス</i>」をクリックしてSNMPなどのパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/windows + en-US: https://hertzbeat.apache.org/docs/help/windows +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 161 + # field-param field key + - field: snmpVersion + # name-param field display i18n name + name: + zh-CN: SNMP 版本 + en-US: SNMP Version + ja-JP: SNMPバージョン + # type-param field type(radio mapping the html radio tag) + type: radio + # required-true or false + required: true + # when type is radio checkbox, use option to show optional values {name1:value1,name2:value2} + options: + - label: SNMPv1 + value: 0 + - label: SNMPv2c + value: 1 + # field-param field key + - field: community + # name-param field display i18n name + name: + zh-CN: SNMP 团体字 + en-US: SNMP Community + ja-JP: SNMPコミュニティ + # type-param field type(most mapping the html input type) + type: text + # when type is text, use limit to limit string length + limit: 100 + # required-true or false + required: true + # param field input placeholder + placeholder: 'Snmp community for v1 v2c' + # dependent parameter values list + depend: + snmpVersion: + - 0 + - 1 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 超时时间(ms) + en-US: Timeout(ms) + ja-JP: タイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,100000]' + # required-true or false + required: false + # hide-is hide this field and put it in advanced layout + hide: true + # default value + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - system + - name: system + i18n: + zh-CN: 系统 + en-US: System + ja-JP: システム情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + fields: + - field: name + type: 1 + i18n: + zh-CN: 名称 + en-US: Name + ja-JP: 名前 + - field: descr + type: 1 + i18n: + zh-CN: 描述 + en-US: Description + ja-JP: 説明 + - field: uptime + type: 1 + i18n: + zh-CN: 运行时间 + en-US: Uptime + ja-JP: アップタイム + - field: numUsers + type: 0 + i18n: + zh-CN: 用户数 + en-US: Number of Users + ja-JP: ユーザー数 + - field: services + type: 0 + i18n: + zh-CN: 服务数 + en-US: Number of Services + ja-JP: サービス数 + - field: processes + type: 0 + i18n: + zh-CN: 进程数 + en-US: Number of Processes + ja-JP: プロセス数 + - field: responseTime + type: 0 + i18n: + zh-CN: 响应时间 + en-US: Response Time + ja-JP: 応答時間 + unit: ms + - field: location + type: 1 + i18n: + zh-CN: 位置 + en-US: Location + ja-JP: 位置 + - field: memory + type: 0 + i18n: + zh-CN: 内存 + en-US: Memory + ja-JP: メモリ + unit: GB + units: + - memory=KB->GB + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^version^_^ + operation: get + oids: + name: 1.3.6.1.2.1.1.5.0 + descr: 1.3.6.1.2.1.1.1.0 + uptime: 1.3.6.1.2.1.25.1.1.0 + numUsers: 1.3.6.1.2.1.25.1.5.0 + services: 1.3.6.1.2.1.1.7.0 + processes: 1.3.6.1.2.1.25.1.6.0 + location: 1.3.6.1.2.1.1.6.0 + memory: 1.3.6.1.2.1.25.2.2.0 + + - name: processes + i18n: + zh-CN: 进程 + en-US: Processes + ja-JP: プロセス + priority: 1 + fields: + - field: hrSWRunIndex + type: 0 + i18n: + zh-CN: 编号 + en-US: Index + ja-JP: 番号 + - field: hrSWRunName + type: 1 + i18n: + zh-CN: 进程名称 + en-US: Process Name + ja-JP: プロセス名 + label: true + - field: hrSWRunID + type: 1 + i18n: + zh-CN: 进程 ID + en-US: Process ID + ja-JP: プロセスID + - field: hrSWRunPath + type: 1 + i18n: + zh-CN: 进程路径 + en-US: Process Path + ja-JP: プロセスパス + - field: hrSWRunParameters + type: 1 + i18n: + zh-CN: 进程参数 + en-US: Process Parameters + ja-JP: プロセスパラメータ + - field: hrSWRunType + type: 0 + i18n: + zh-CN: 进程类型 + en-US: Process Type + ja-JP: プロセスタイプ + - field: hrSWRunStatus + type: 0 + i18n: + zh-CN: 进程状态 + en-US: Process Status + ja-JP: プロセス状態 + - field: hrSWRunPerfCPU + type: 0 + i18n: + zh-CN: 进程占用 CPU + en-US: Process CPU + ja-JP: プロセスが使用したCPU + unit: 's' + - field: hrSWRunPerfMem + type: 0 + i18n: + zh-CN: 进程占用内存 + en-US: Process Memory + ja-JP: プロセスが使用したメモリ + unit: 'KB' + calculates: + - hrSWRunPerfCPU = hrSWRunPerfCPU / 100 + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + hrSWRunIndex: 1.3.6.1.2.1.25.4.2.1.1 + hrSWRunName: 1.3.6.1.2.1.25.4.2.1.2 + hrSWRunID: 1.3.6.1.2.1.25.4.2.1.3 + hrSWRunPath: 1.3.6.1.2.1.25.4.2.1.4 + hrSWRunParameters: 1.3.6.1.2.1.25.4.2.1.5 + hrSWRunType: 1.3.6.1.2.1.25.4.2.1.6 + hrSWRunStatus: 1.3.6.1.2.1.25.4.2.1.7 + hrSWRunPerfCPU: 1.3.6.1.2.1.25.5.1.1.1 + hrSWRunPerfMem: 1.3.6.1.2.1.25.5.1.1.2 + + - name: services + i18n: + zh-CN: 服务 + en-US: Services + ja-JP: サービス + priority: 2 + fields: + - field: svSvcName + type: 1 + i18n: + zh-CN: 服务名称 + en-US: Service Name + ja-JP: サービス名 + label: true + - field: svSvcInstalledState + type: 0 + i18n: + zh-CN: 服务安装状态 + en-US: Service Installed State + ja-JP: サービスのインストール状態 + - field: svSvcOperatingState + type: 0 + i18n: + zh-CN: 服务运行状态 + en-US: Service Operating State + ja-JP: サービスの実行状態 + - field: svSvcCanBeUninstalled + type: 0 + i18n: + zh-CN: 服务是否可以被卸载 + en-US: Service Can Be Uninstalled + ja-JP: サービスはアンインストールできる + - field: svSvcCanBePaused + type: 0 + i18n: + zh-CN: 服务是否可以被暂停 + en-US: Service Can Be Paused + ja-JP: サービスは一時停止できる + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^version^_^ + operation: walk + oids: + svSvcName: 1.3.6.1.4.1.77.1.2.3.1.1 + svSvcInstalledState: 1.3.6.1.4.1.77.1.2.3.1.2 + svSvcOperatingState: 1.3.6.1.4.1.77.1.2.3.1.3 + svSvcCanBeUninstalled: 1.3.6.1.4.1.77.1.2.3.1.4 + svSvcCanBePaused: 1.3.6.1.4.1.77.1.2.3.1.5 + + - name: installed + i18n: + zh-CN: 安装 + en-US: Installed + ja-JP: インストール + priority: 3 + fields: + - field: hrSWInstalledIndex + type: 0 + i18n: + zh-CN: 编号 + en-US: Index + ja-JP: 番号 + - field: hrSWInstalledName + type: 1 + i18n: + zh-CN: 安装名称 + en-US: Installed Name + ja-JP: インストール名 + label: true + - field: hrSWInstalledID + type: 1 + i18n: + zh-CN: 安装 ID + en-US: Installed ID + ja-JP: インストールID + - field: hrSWInstalledType + type: 0 + i18n: + zh-CN: 安装类型 + en-US: Installed Type + ja-JP: インストールタイプ + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^version^_^ + operation: walk + oids: + hrSWInstalledIndex: 1.3.6.1.2.1.25.6.3.1.1 + hrSWInstalledName: 1.3.6.1.2.1.25.6.3.1.2 + hrSWInstalledID: 1.3.6.1.2.1.25.6.3.1.3 + hrSWInstalledType: 1.3.6.1.2.1.25.6.3.1.4 + + - name: storages + i18n: + zh-CN: 存储 + en-US: Storages + ja-JP: ストレージ + priority: 5 + fields: + - field: index + type: 0 + i18n: + zh-CN: 编号 + en-US: Index + ja-JP: 番号 + - field: descr + type: 1 + i18n: + zh-CN: 存储描述 + en-US: Storage Description + ja-JP: ストレージ説明 + label: true + - field: Size + i18n: + zh-CN: 存储大小 + en-US: Storage Size + ja-JP: ストレージサイズ + type: 0 + unit: Mb + - field: free + type: 0 + i18n: + zh-CN: 存储空闲 + en-US: Storage Free + ja-JP: 空きストレージ + unit: Mb + - field: used + type: 0 + i18n: + zh-CN: 存储占用 + en-US: Storage Used + ja-JP: 使用したストレージ + unit: Mb + - field: usage + type: 0 + i18n: + zh-CN: 存储使用率 + en-US: Storage Usage + ja-JP: ストレージ使用率 + unit: '%' + # (Not required) Monitor indicator alias, which maps to the indicator name above. The field used to collect interface data is not directly the final indicator name, and this alias is required for mapping translation + aliasFields: + - hrStorageIndex + - hrStorageDescr + - hrStorageSize + - hrStorageUsed + - hrStorageAllocationUnits + # The (optional) metric evaluation expression, which works with the aliases above, calculates the final desired metric value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - index=hrStorageIndex + - descr=hrStorageDescr + - Size=hrStorageSize * hrStorageAllocationUnits + - free=(hrStorageSize - hrStorageUsed) * hrStorageAllocationUnits + - used=hrStorageUsed * hrStorageAllocationUnits + - usage= hrStorageUsed / hrStorageSize * 100 + units: + - size=B->Mb + - used=B->Mb + - free=B->Mb + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^version^_^ + operation: walk + oids: + hrStorageDescr: 1.3.6.1.2.1.25.2.3.1.3 + hrStorageIndex: 1.3.6.1.2.1.25.2.3.1.1 + hrStorageSize: 1.3.6.1.2.1.25.2.3.1.5 + hrStorageUsed: 1.3.6.1.2.1.25.2.3.1.6 + hrStorageAllocationUnits: 1.3.6.1.2.1.25.2.3.1.4 + + + - name: interfaces + i18n: + zh-CN: 接口详情 + en-US: Interfaces Detail + ja-JP: ネットワークカード詳細 + priority: 8 + fields: + - field: index + type: 1 + i18n: + zh-CN: 编号 + en-US: Index + ja-JP: 番号 + - field: descr + type: 1 + label: true + i18n: + zh-CN: 接口名称 + en-US: Interface Name + ja-JP: ネットワークカード名 + - field: speed + type: 0 + unit: 'Mbps' + i18n: + zh-CN: 接口速率 + en-US: Interface Speed + ja-JP: ネットワークカード速度 + - field: in_octets + type: 0 + unit: 'Kbps' + i18n: + zh-CN: 入流量 + en-US: In Octets + ja-JP: 受信されたバイト数 + - field: out_octets + type: 0 + unit: 'Kbps' + i18n: + zh-CN: 出流量 + en-US: Out Octets + ja-JP: 転送されたバイト数 + - field: admin_status + type: 1 + i18n: + zh-CN: 配置状态 + en-US: Config Status + ja-JP: 設定ステータス + - field: oper_status + type: 1 + i18n: + zh-CN: 当前状态 + en-US: Current Status + ja-JP: 現在のステータス + - field: mtu + type: 0 + i18n: + zh-CN: MTU + en-US: MTU + ja-JP: MTU + - field: in_discards + type: 0 + i18n: + zh-CN: 入丢包数 + en-US: In Discards + ja-JP: 受信されたパケットのロス数 + - field: in_errors + type: 0 + i18n: + zh-CN: 入错包数 + en-US: In Errors + ja-JP: 受信異常パケット数 + - field: out_discards + type: 0 + i18n: + zh-CN: 出丢包数 + en-US: Out Discards + ja-JP: 転送されたパケットロス数 + - field: out_errors + type: 0 + i18n: + zh-CN: 出错包数 + en-US: Out Errors + ja-JP: 転送された異常パケット数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - ifIndex + - ifDescr + - ifSpeed + - ifInOctets + - ifOutOctets + - ifAdminStatus + - ifOperStatus + - ifMtu + - ifInDiscards + - ifInErrors + - ifOutDiscards + - ifOutErrors + # mapping and conversion expressions, use these and aliasField above to calculate metrics value + # eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime + calculates: + - index=ifIndex + - descr=ifDescr + - speed=ifSpeed / 1000 / 1000 + - in_octets=ifInOctets / 1000 + - out_octets=ifOutOctets / 1000 + - admin_status=ifAdminStatus + - oper_status=ifOperStatus + - mtu=ifMtu + - in_discards=ifInDiscards + - in_errors=ifInErrors + - out_discards=ifOutDiscards + - out_errors=ifOutErrors + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + username: ^_^username^_^ + authPassphrase: ^_^authPassphrase^_^ + privPassphrase: ^_^privPassphrase^_^ + privPasswordEncryption: ^_^privPasswordEncryption^_^ + authPasswordEncryption: ^_^authPasswordEncryption^_^ + contextName: ^_^contextName^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^snmpVersion^_^ + operation: walk + oids: + ifIndex: 1.3.6.1.2.1.2.2.1.1 + ifDescr: 1.3.6.1.2.1.2.2.1.2 + ifSpeed: 1.3.6.1.2.1.2.2.1.5 + ifInOctets: 1.3.6.1.2.1.2.2.1.10 + ifOutOctets: 1.3.6.1.2.1.2.2.1.16 + ifAdminStatus: 1.3.6.1.2.1.2.2.1.7 + ifOperStatus: 1.3.6.1.2.1.2.2.1.8 + ifMtu: 1.3.6.1.2.1.2.2.1.4 + ifInDiscards: 1.3.6.1.2.1.2.2.1.13 + ifInErrors: 1.3.6.1.2.1.2.2.1.14 + ifOutDiscards: 1.3.6.1.2.1.2.2.1.19 + ifOutErrors: 1.3.6.1.2.1.2.2.1.20 + + - name: devices + i18n: + zh-CN: 设备 + en-US: Devices + ja-JP: デバイス + priority: 9 + fields: + - field: index + type: 1 + i18n: + zh-CN: 编号 + en-US: Index + ja-JP: 番号 + label: true + - field: descr + type: 1 + i18n: + zh-CN: 描述 + en-US: Description + ja-JP: 説明 + - field: status + type: 1 + i18n: + zh-CN: 状态 + en-US: Status + ja-JP: 状態 + aliasFields: + - hrDeviceIndex + - hrDeviceDescr + - hrDeviceStatus + calculates: + - index=hrDeviceIndex + - descr=hrDeviceDescr + - status=hrDeviceStatus + protocol: snmp + snmp: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + community: ^_^community^_^ + version: ^_^version^_^ + operation: walk + oids: + hrDeviceIndex: 1.3.6.1.2.1.25.3.2.1.1 + hrDeviceDescr: 1.3.6.1.2.1.25.3.2.1.3 + hrDeviceStatus: 1.3.6.1.2.1.25.3.2.1.5 diff --git a/hertzbeat-manager/src/main/resources/define/app-windows_script.yml b/hertzbeat-manager/src/main/resources/define/app-windows_script.yml new file mode 100644 index 0000000..511618f --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-windows_script.yml @@ -0,0 +1,370 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: os +# The monitoring type eg: linux windows tomcat mysql aws... +app: windows_script +# The monitoring i18n name +name: + zh-CN: Windows 命令 + en-US: Windows Script + ja-JP: Windows Script +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 使用采集器作为 agent 直接运行 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/windows_script'> CMD 或 Powershell 命令 </a> 对 Windows 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Windows Script</i>”进行添加。或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat uses a collector as an agent to directly execute <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/windows_script'> CMD or Powershell commands </a> to collect and monitor general performance metrics of the Windows operating system (system information, CPU, memory, disk, network card, file system, TOP resource processes, etc.).<br>You can click “<i>Create New Windows Script</i>” to add it. Or select “<i>More Actions</i>” to import an existing configuration. + zh-TW: Hertzbeat 使用采集器作為 agent 直接運行 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/windows_script'> CMD 或 Powershell 命令 </a> 對 Windows 操作系統的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Windows Script</i>”進行添加。或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は agent として <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/windows_script'> CMD か Powershell コマンドを介して </a> Windowsオーエスの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Windows Script</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。 +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/windows_script + en-US: https://hertzbeat.apache.org/docs/help/windows_script +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true +# collect metrics config list +metrics: + # metrics - basic, inner monitoring metrics (responseTime - response time) + - name: basic + i18n: + zh-CN: 系统基本信息 + en-US: Basic Info + ja-JP: 基礎情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: hostname + type: 1 + label: true + i18n: + zh-CN: 主机名称 + en-US: Host Name + ja-JP: ホスト名 + - field: version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: System Version + ja-JP: オーエスバージョン + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: script + # the config content when protocol is ssh + script: + # script tool + scriptTool: cmd + # OS charset + charset: GBK + # script working directory + workDirectory: "C:" + # collect script + scriptCommand: + | + @echo %COMPUTERNAME% & for /f "tokens=4 delims=[] " %i in ('ver') do @echo %i + # response data parse type: oneRow, multiRow + parseType: oneRow + + - name: cpu + i18n: + zh-CN: CPU 信息 + en-US: CPU Info + ja-JP: CPU情報 + priority: 1 + fields: + - field: info + type: 1 + i18n: + zh-CN: 型号 + en-US: Info + ja-JP: バージョン + - field: cores + type: 1 + i18n: + zh-CN: 核数 + en-US: Cores + ja-JP: コア数 + - field: load + type: 1 + unit: '%' + i18n: + zh-CN: 负载 + en-US: Load + ja-JP: ロード + - field: interrupt + type: 1 + i18n: + zh-CN: 中断数 + en-US: Interrupt + ja-JP: 割り込み数 + - field: context_switch + type: 1 + i18n: + zh-CN: 上下文切换 + en-US: Context Switch + ja-JP: コンテキストスイッチ + - field: usage + type: 1 + unit: '%' + i18n: + zh-CN: 使用率 + en-US: Usage + ja-JP: 使用率 + protocol: script + script: + scriptTool: cmd + charset: GBK + scriptCommand: + | + for /f "tokens=2 delims==" %i in ('wmic cpu get Name /value ^| find "Name"') do @echo %i&&for /f "tokens=2 delims==" %i in ('wmic cpu get NumberOfLogicalProcessors /value ^| find "NumberOfLogicalProcessors"') do @echo %i&&for /f "tokens=2 delims==" %i in ('wmic cpu get loadpercentage /value') do @echo %i&&for /f "skip=2 tokens=2 delims=," %a in ('typeperf "\Processor Information(_Total)\Interrupts/sec" -sc 1') do @echo %a && for /f "skip=2 tokens=2 delims=," %a in ('typeperf "\System\Context Switches/sec" -sc 1') do @echo %a&&for /f "skip=2 tokens=2 delims=," %a in ('typeperf "\Processor(_Total)\% Processor Time" -sc 1') do @echo %a + parseType: oneRow + + - name: memory + i18n: + zh-CN: 内存信息 + en-US: Memory Info + ja-JP: メモリ情報 + priority: 2 + fields: + - field: totalPhysical + type: 0 + unit: Mb + i18n: + zh-CN: 总物理内存容量 + en-US: Total Physical Memory + ja-JP: 物理メモリ容量 + - field: freePhysical + type: 0 + unit: Mb + i18n: + zh-CN: 空闲物理内存容量 + en-US: Free Physical Memory + ja-JP: 使用可能な物理メモリ + - field: totalVirtual + type: 0 + unit: Mb + i18n: + zh-CN: 总虚拟内存容量 + en-US: Total Virtual Memory + ja-JP: 仮想メモリ容量 + - field: freeVirtual + type: 0 + unit: Mb + i18n: + zh-CN: 空闲虚拟内存容量 + en-US: Free Virtual Memory + ja-JP: 使用可能な仮想メモリ + units: + - totalPhysical=B->MB + - freePhysical=KB->MB + - totalVirtual=KB->MB + - freeVirtual=KB->MB + protocol: script + script: + scriptTool: cmd + charset: GBK + scriptCommand: + | + for /f "tokens=2 delims==" %i in ('wmic computerSystem get TotalPhysicalMemory /value ^| find "TotalPhysicalMemory"') do @echo %i&&for /f "tokens=2 delims==" %i in ('wmic os get FreePhysicalMemory /value ^| find "FreePhysicalMemory"') do @echo %i&&for /f "tokens=2 delims==" %i in ('wmic os get TotalVirtualMemorySize /value ^| find "TotalVirtualMemorySize"') do @echo %i&&for /f "tokens=2 delims==" %i in ('wmic os get FreeVirtualMemory /value ^| find "FreeVirtualMemory"') do @echo %i + parseType: oneRow + + - name: disk + i18n: + zh-CN: 磁盘信息 + en-US: Disk Info + ja-JP: ディスク情報 + priority: 3 + fields: + - field: Model + type: 1 + i18n: + zh-CN: 磁盘型号 + en-US: Disk Type + ja-JP: ディスク番号 + - field: Size + type: 0 + unit: Mb + i18n: + zh-CN: 磁盘大小 + en-US: Disk Size + ja-JP: ディスクサイズ + - field: BytesPerSector + type: 0 + i18n: + zh-CN: 每个扇区的字节数 + en-US: Bytes Per Sector + ja-JP: 各セクターのバイト数 + protocol: script + script: + scriptTool: powershell + charset: GBK + scriptCommand: + | + Write-Host "Model Size BytesPerSector"; + Get-WmiObject -Class Win32_DiskDrive | ForEach-Object { + $model = $_.Model -replace ' ', '_' + $size = [math]::round($_.Size / 1MB, 0) + $bytesPerSector = $_.BytesPerSector + Write-Host "$model $size $bytesPerSector" + } + parseType: multiRow + + - name: disk_free + i18n: + zh-CN: 文件系统 + en-US: Disk Free + ja-JP: ファイルシステム + priority: 4 + fields: + - field: Caption + type: 1 + i18n: + zh-CN: 盘符 + en-US: Caption + ja-JP: キャプション + - field: FreeSpace + type: 0 + unit: Mb + i18n: + zh-CN: 可用量 + en-US: FreeSpace + ja-JP: 空き容量 + - field: Size + type: 0 + unit: Mb + i18n: + zh-CN: 总量 + en-US: Size + ja-JP: サイズ + units: + - FreeSpace=B->MB + - Size=B->MB + protocol: script + script: + scriptTool: cmd + charset: GBK + scriptCommand: + | + wmic logicaldisk get size,freespace,caption + parseType: multiRow + + - name: top_cpu_process + i18n: + zh-CN: Top10 CPU 进程 + en-US: Top10 CPU Process + ja-JP: トップ10 CPUプロセス + priority: 5 + fields: + - field: name + type: 1 + i18n: + zh-CN: 进程名 + en-US: Name + ja-JP: プロセス名 + - field: id + type: 1 + i18n: + zh-CN: 进程 ID + en-US: PID + ja-JP: プロセスID + - field: cpu + type: 0 + unit: 's' + i18n: + zh-CN: CPU 使用时间 + en-US: CPU Usage Time + ja-JP: CPU使用時間 + - field: ws + type: 0 + unit: Mb + i18n: + zh-CN: 内存占用量 + en-US: Memory Usage + ja-JP: メモリ使用率 + protocol: script + script: + scriptTool: powershell + charset: GBK + scriptCommand: + | + Write-Host "name id cpu ws"; + Get-Process | Sort-Object -Property CPU -Descending | Select-Object -First 10 | ForEach-Object { + $name = $_.Name -replace ' ', '_' + $id = $_.Id + $cpu = [math]::round($_.CPU, 0) + $ws = [math]::round($_.WS / 1MB, 0) + Write-Host "$name $id $cpu $ws" + } + parseType: multiRow + + - name: top_mem_process + i18n: + zh-CN: Top10 内存进程 + en-US: Top10 Memory Process + ja-JP: トップ10 メモリプロセス + priority: 6 + fields: + - field: name + type: 1 + i18n: + zh-CN: 进程名称 + en-US: Process Name + ja-JP: プロセス名 + - field: id + type: 1 + i18n: + zh-CN: 进程 ID + en-US: PID + ja-JP: プロセスID + - field: ws + type: 0 + unit: Mb + i18n: + zh-CN: 内存占用量 + en-US: Memory Usage + ja-JP: メモリ使用率 + - field: cpu + type: 0 + unit: s + i18n: + zh-CN: CPU 占用时间 + en-US: CPU Usage Time + ja-JP: CPU使用時間 + protocol: script + script: + scriptTool: powershell + charset: GBK + scriptCommand: + | + Write-Host "name id ws cpu"; + Get-Process | Sort-Object -Property WS -Descending | Select-Object -First 10 | ForEach-Object { + $name = $_.Name -replace ' ', '_' + $id = $_.Id + $ws = [math]::round($_.WS / 1MB, 0) + $cpu = [math]::round($_.CPU, 0) + Write-Host "$name $id $ws $cpu" + } + parseType: multiRow diff --git a/hertzbeat-manager/src/main/resources/define/app-xugu.yml b/hertzbeat-manager/src/main/resources/define/app-xugu.yml new file mode 100644 index 0000000..0b15662 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-xugu.yml @@ -0,0 +1,321 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: db +app: xugu +# The monitoring i18n name +name: + zh-CN: 虚谷数据库 + en-US: Xugu DB +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 虚谷 数据库的通用性能指标(会话 信息、内存 信息、磁盘 信息)进行采集监控,支持版本为 XuguDB V12。<br>您可以点击“<i>新建 虚谷数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: HertzBeat uses the <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'>JDBC protocol</a> to collect and monitor general performance metrics (session information, memory information, disk information) for Xugu Database by configuring SQL. It supports XuguDB V12. +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/xugu + en-US: https://hertzbeat.apache.org/docs/help/xugu +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 5138 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[400,200000]' + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 + # field-param field key + - field: database + # name-param field display i18n name + name: + zh-CN: 数据库名称 + en-US: Database Name + # type-param field type(most mapping the html input tag) + type: text + # required-true or false + required: false + # field-param field key + - field: username + # name-param field display i18n name + name: + zh-CN: 用户名 + en-US: Username + # type-param field type(most mapping the html input tag) + type: text + # when type is text, use limit to limit string length + limit: 50 + # required-true or false + required: false + # field-param field key + - field: password + # name-param field display i18n name + name: + zh-CN: 密码 + en-US: Password + # type-param field type(most mapping the html input tag) + type: password + # required-true or false + required: false + # field-param field key + +# collect metrics config list +metrics: + # metrics - basic + - name: session_information + priority: 0 + i18n: + zh-CN: 会话 信息 + en-US: Session Info + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: Idle_Connection + type: 0 + label: true + i18n: + zh-CN: 空闲连接 + en-US: Idle Connection + - field: Active_Connections + type: 0 + i18n: + zh-CN: 活跃连接 + en-US: Active Connections + - field: Connection_being_created + type: 0 + i18n: + zh-CN: 正在创建的连接 + en-US: Connection being created + - field: Null_Read_Connection + type: 0 + i18n: + zh-CN: 空读连接 + en-US: Null Read Connection + - field: Complete_data_input_connection + type: 0 + i18n: + zh-CN: 完成数据输入连接 + en-US: Complete data input connection + - field: Other + type: 0 + i18n: + zh-CN: 其他 + en-US: Other + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: xugu + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # database name + database: ^_^database^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + queryType: columns + # sql + sql: SELECT CASE status WHEN 112 THEN 'Idle_Connection' WHEN 114 THEN 'Active_Connections' WHEN 111 THEN 'Connection_being_created' WHEN 113 THEN 'Null_Read_Connection' WHEN 115 THEN 'Complete_data_input_connection' ELSE 'Other' END AS status_type, COUNT(*) AS count FROM sys_sessions GROUP BY status; + # JDBC url + url: ^_^url^_^ + + - name: Memory + priority: 1 + # collect metrics content + i18n: + zh-CN: 内存 信息 + en-US: Memory Info + fields: + - field: G_MEM + type: 0 + unit: MB + i18n: + zh-CN: 全局内存消耗 + en-US: Global Memory Consumption + - field: CATA_MEM + type: 0 + unit: MB + i18n: + zh-CN: CATALOG内存消耗 + en-US: CATALOG memory consumption + - field: TRAN_MEM + type: 0 + unit: MB + i18n: + zh-CN: 事务内存消耗 + en-US: Transactional memory consumption + - field: NET_MEM + type: 0 + unit: MB + i18n: + zh-CN: 用户连接用内存消耗 + en-US: Memory consumption for user connections + - field: TASK_MEM + type: 0 + unit: MB + i18n: + zh-CN: 任务线程消耗 + en-US: Task Thread Consumption + - field: MSG_MEM + type: 0 + unit: MB + i18n: + zh-CN: 内部通讯消息内存消耗 + en-US: Internal communication message memory consumption + - field: LOCK_MEM + type: 0 + unit: MB + i18n: + zh-CN: 局部锁内存消耗 + en-US: Local lock memory consumption + - field: GLOCK_MEM + type: 0 + unit: MB + i18n: + zh-CN: 全局锁内存消耗 + en-US: Global lock memory consumption + - field: DLCHK_MEM + type: 0 + unit: MB + i18n: + zh-CN: 死锁检测内存消耗 + en-US: Deadlock detection memory consumption + - field: MODI_MEM + type: 0 + unit: MB + i18n: + zh-CN: 变更日志内存消耗 + en-US: Change Log Memory Consumption + - field: PROC_MEM + type: 0 + unit: MB + i18n: + zh-CN: 过程执行机内存消耗 + en-US: Process Execution Machine Memory Consumption + units: + - G_MEM=B->MB + - CATA_MEM=B->MB + - TRAN_MEM=B->MB + - NET_MEM=B->MB + - TASK_MEM=B->MB + - MSG_MEM=B->MB + - LOCK_MEM=B->MB + - GLOCK_MEM=B->MB + - DLCHK_MEM=B->MB + - MODI_MEM=B->MB + - PROC_MEM=B->MB + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: jdbc + # the config content when protocol is jdbc + jdbc: + # jdbc host + # jdbchost + host: ^_^host^_^ + # port + port: ^_^port^_^ + # platform name + platform: xugu + # username + username: ^_^username^_^ + # password + password: ^_^password^_^ + # database name + database: ^_^database^_^ + # timeout unit:ms + timeout: ^_^timeout^_^ + # SQL Query Method:oneRow, multiRow, columns + queryType: columns + # sql + sql: SELECT targ_name,targ_value from sys_monitors where targ_name like '%MEM'; + url: ^_^url^_^ + + - name: Disk + priority: 1 + i18n: + zh-CN: 磁盘 信息 + en-US: Disk Info + fields: + - field: DISK_READ_NUM + type: 0 + unit: Times + i18n: + zh-CN: 数据块磁盘读取次数 + en-US: Number of data block disk reads + - field: DISK_WRITE_NUM + type: 0 + unit: Times + i18n: + zh-CN: 磁盘写入数据块次数 + en-US: Number of data blocks written to disk + - field: DISK_READ_BYTES + type: 0 + unit: MB + i18n: + zh-CN: 磁盘读取字节数 + en-US: Disk read bytes + - field: DISK_WRITE_BYTES + type: 0 + unit: MB + i18n: + zh-CN: 磁盘写入字节数 + en-US: Bytes written to disk + units: + - DISK_READ_BYTES=B->MB + - DISK_WRITE_BYTES=B->MB + protocol: jdbc + jdbc: + host: ^_^host^_^ + port: ^_^port^_^ + platform: xugu + username: ^_^username^_^ + password: ^_^password^_^ + database: ^_^database^_^ + timeout: ^_^timeout^_^ + queryType: columns + sql: SELECT targ_name,targ_value from sys_monitors where targ_name like 'DISK_%'; + url: ^_^url^_^ diff --git a/hertzbeat-manager/src/main/resources/define/app-yarn.yml b/hertzbeat-manager/src/main/resources/define/app-yarn.yml new file mode 100644 index 0000000..4435d05 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-yarn.yml @@ -0,0 +1,528 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: bigdata +# The monitoring type eg: linux windows tomcat mysql aws... +app: yarn +# The monitoring i18n name +name: + zh-CN: Apache Yarn + en-US: Apache Yarn + ja-JP: Apache Yarn +# The description and help of this monitoring type +help: + zh-CN: Hertzbeat 对 Yarn 节点监控指标进行监控。<br>您可以点击 “<i>新建 Apache Yarn</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。 + en-US: Hertzbeat monitors the Yarn metrics. <br>You can click "<i>New Apache Yarn</i>" to configure, or select "<i>More Actions</i>" to import an existing configuration. + zh-TW: Hertzbeat 對 Yarn 節點監控指標進行監控。<br>您可以點擊 “<i>新建 Apache Yarn</i>” 並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。 + ja-JP: Hertzbeat は Yarnノードの一般的なメトリクスを監視します。<br>「<i>新規 Yarn</i>」をクリックしてパラメタを設定した後、新規することができます。 + +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/yarn/ + en-US: https://hertzbeat.apache.org/docs/help/yarn/ +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 8088 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间 + en-US: Query Timeout + ja-JP: クエリタイムアウト + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide param-true or false + hide: true + # default value + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - Server + - name: ClusterMetrics + i18n: + zh-CN: 集群指标 + en-US: Cluster Metrics + ja-JP: クラスターメトリクス + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: NumActiveNMs + type: 0 + i18n: + zh-CN: 当前存活的 NodeManager 个数 + en-US: NumActiveNMs + ja-JP: アクティブなNodeManager総数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: NumDecommissionedNMs + type: 0 + i18n: + zh-CN: 当前 Decommissioned 的 NodeManager 个数 + en-US: NumDecommissionedNMs + ja-JP: オフラインしたNodeManager数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: NumDecommissioningNMs + type: 0 + i18n: + zh-CN: 集群正在下线的节点数 + en-US: NumDecommissioningNMs + ja-JP: オフライン中のNodeManager数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: NumLostNMs + type: 0 + i18n: + zh-CN: 集群丢失的节点数 + en-US: NumLostNMs + ja-JP: ロストしたNodeManager数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: NumUnhealthyNMs + type: 0 + i18n: + zh-CN: 集群不健康的节点数 + en-US: NumUnhealthyNMs + ja-JP: 不健全なNodeManager数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.NumActiveNMs + - $.NumDecommissionedNMs + - $.NumDecommissioningNMs + - $.NumLostNMs + - $.NumUnhealthyNMs + - $.RpcProcessingTimeAvgTime + - $.CallQueueLength + calculates: + - NumActiveNMs=$.NumActiveNMs + - NumDecommissionedNMs=$.NumDecommissionedNMs + - NumDecommissioningNMs=$.NumDecommissioningNMs + - NumLostNMs=$.NumLostNMs + - NumUnhealthyNMs=$.NumUnhealthyNMs + - RpcProcessingTimeAvgTime=$.RpcProcessingTimeAvgTime + - CallQueueLength=$.CallQueueLength + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name == "Hadoop:service=ResourceManager,name=ClusterMetrics")]' + - name: JvmMetrics + i18n: + zh-CN: JVM指标 + en-US: Jvm Metrics + ja-JP: Java仮想マシンのメトリクス + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 1 + # collect metrics content + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemNonHeapCommittedM + type: 0 + unit: MB + i18n: + zh-CN: JVM当前非堆内存大小已提交大小 + en-US: MemNonHeapCommittedM + ja-JP: コミットされたノンヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemNonHeapMaxM + type: 0 + unit: MB + i18n: + zh-CN: JVM非堆最大可用内存 + en-US: MemNonHeapMaxM + ja-JP: 使用可能なノンヒープメモリの最大サイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemNonHeapUsedM + type: 0 + unit: MB + i18n: + zh-CN: JVM当前已使用的非堆内存大小 + en-US: MemNonHeapUsedM + ja-JP: 使用したノンヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemHeapCommittedM + type: 0 + unit: MB + i18n: + zh-CN: JVM当前已使用堆内存大小 + en-US: MemHeapCommittedM + ja-JP: コミットされたヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemHeapMaxM + type: 0 + unit: MB + i18n: + zh-CN: JVM堆内存最大可用内存 + en-US: MemHeapMaxM + ja-JP: 配置のヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: MemHeapUsedM + type: 0 + unit: MB + i18n: + zh-CN: JVM当前已使用堆内存大小 + en-US: MemHeapUsedM + ja-JP: 使用したヒープメモリのサイズ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: GcTimeMillis + type: 0 + i18n: + zh-CN: JVM GC时间 + en-US: GcTimeMillis + ja-JP: GC時間 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: GcCount + type: 0 + i18n: + zh-CN: JVM GC次数 + en-US: GcCount + ja-JP: GC回数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.MemNonHeapCommittedM + - $.MemNonHeapMaxM + - $.MemNonHeapUsedM + - $.MemHeapCommittedM + - $.MemHeapMaxM + - $.MemHeapUsedM + - $.GcTimeMillis + - $.GcCount + calculates: + - MemNonHeapCommittedM=$.MemNonHeapCommittedM + - MemNonHeapMaxM=$.MemNonHeapMaxM + - MemNonHeapUsedM=$.MemNonHeapUsedM + - MemHeapCommittedM=$.MemHeapCommittedM + - MemHeapMaxM=$.MemHeapMaxM + - MemHeapUsedM=$.MemHeapUsedM + - GcTimeMillis=$.GcTimeMillis + - GcCount=$.GcCount + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name == "Hadoop:service=ResourceManager,name=JvmMetrics")]' + - name: QueueMetrics + i18n: + zh-CN: 队列指标 + en-US: Queue Metrics + ja-JP: キューメトリクス + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 2 + # collect metrics content + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: queue + type: 1 + label: true + i18n: + zh-CN: 队列名称 + en-US: queue + ja-JP: キュー + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AllocatedVCores + type: 0 + i18n: + zh-CN: 分配的虚拟核数(已分配) + en-US: AllocatedVCores + ja-JP: 割り当てられたコア数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ReservedVCores + type: 0 + i18n: + zh-CN: 预留核数 + en-US: ReservedVCores + ja-JP: 予約されているコア数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AvailableVCores + type: 0 + i18n: + zh-CN: 可用核数(尚未分配) + en-US: AvailableVCores + ja-JP: 利用可能なコア数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: PendingVCores + type: 0 + i18n: + zh-CN: 阻塞调度核数 + en-US: PendingVCores + ja-JP: 保留中のコア数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AllocatedMB + type: 0 + unit: MB + i18n: + zh-CN: 已分配(已用)的内存大小 + en-US: AllocatedMB + ja-JP: 割り当てられたメモリ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AvailableMB + type: 0 + unit: MB + i18n: + zh-CN: 可用内存(尚未分配) + en-US: AvailableMB + ja-JP: 使用可能なメモリ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: PendingMB + type: 0 + unit: MB + i18n: + zh-CN: 阻塞调度内存 + en-US: PendingMB + ja-JP: 保留中のメモリ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ReservedMB + type: 0 + unit: MB + i18n: + zh-CN: 预留内存 + en-US: ReservedMB + ja-JP: 予約されているメモリ + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AllocatedContainers + type: 0 + i18n: + zh-CN: 已分配(已用)的container数 + en-US: AllocatedContainers + ja-JP: 割り当てられたコンテナ数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: PendingContainers + type: 0 + i18n: + zh-CN: 阻塞调度container个数 + en-US: PendingContainers + ja-JP: 保留中のコンテナ数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: ReservedContainers + type: 0 + i18n: + zh-CN: 预留container数 + en-US: ReservedContainers + ja-JP: 予約されているコンテナ数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AggregateContainersAllocated + type: 0 + i18n: + zh-CN: 累积的container分配总数 + en-US: AggregateContainersAllocated + ja-JP: 割り当てられたコンテナ総数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AggregateContainersReleased + type: 0 + i18n: + zh-CN: 累积的container释放总数 + en-US: AggregateContainersReleased + ja-JP: リリースされたコンテナ総数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AppsCompleted + type: 0 + i18n: + zh-CN: 完成的任务数 + en-US: AppsCompleted + ja-JP: 完了したタスク数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AppsKilled + type: 0 + i18n: + zh-CN: 被杀掉的任务数 + en-US: AppsKilled + ja-JP: 終了したタスク数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AppsFailed + type: 0 + i18n: + zh-CN: 失败的任务数 + en-US: AppsFailed + ja-JP: 失敗したタスク数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AppsPending + type: 0 + i18n: + zh-CN: 阻塞的任务数 + en-US: AppsPending + ja-JP: ブロックされたタスク数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AppsRunning + type: 0 + i18n: + zh-CN: 提正在运行的任务数 + en-US: AppsRunning + ja-JP: 実行中のタスク数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: AppsSubmitted + type: 0 + i18n: + zh-CN: 提交过的任务数 + en-US: AppsSubmitted + ja-JP: 提出されたタスク数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: running_0 + type: 0 + i18n: + zh-CN: 运行时间小于60分钟的作业个数 + en-US: running_0 + ja-JP: 実行時間60分未満のタスク数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: running_60 + type: 0 + i18n: + zh-CN: 运行时间介于60~300分钟的作业个数 + en-US: running_60 + ja-JP: 実行時間が60分以上300分未満のタスク数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: running_300 + type: 0 + i18n: + zh-CN: 运行时间介于300~1440分钟的作业个数 + en-US: running_300 + ja-JP: 実行時間が300分以上1440分未満のタスク数 + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: running_1440 + type: 0 + i18n: + zh-CN: 运行时间大于1440分钟的作业个数 + en-US: running_1440 + ja-JP: 実行時間が1440分以上のタスク数 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.['tag.Queue'] + - $.AllocatedVCores + - $.ReservedVCores + - $.AvailableVCores + - $.PendingVCores + - $.AllocatedMB + - $.AvailableMB + - $.PendingMB + - $.ReservedMB + - $.AllocatedContainers + - $.PendingContainers + - $.ReservedContainers + - $.AggregateContainersAllocated + - $.AggregateContainersReleased + - $.AppsCompleted + - $.AppsKilled + - $.AppsFailed + - $.AppsPending + - $.AppsRunning + - $.AppsSubmitted + - $.running_0 + - $.running_60 + - $.running_300 + - $.running_1440 + calculates: + - queue=$.['tag.Queue'] + - AllocatedVCores=$.AllocatedVCores + - ReservedVCores=$.ReservedVCores + - AvailableVCores=$.AvailableVCores + - PendingVCores=$.PendingVCores + - AllocatedMB=$.AllocatedMB + - AvailableMB=$.AvailableMB + - PendingMB=$.PendingMB + - ReservedMB=$.ReservedMB + - AllocatedContainers=$.AllocatedContainers + - PendingContainers=$.PendingContainers + - ReservedContainers=$.ReservedContainers + - AggregateContainersAllocated=$.AggregateContainersAllocated + - AggregateContainersReleased=$.AggregateContainersReleased + - AppsCompleted=$.AppsCompleted + - AppsKilled=$.AppsKilled + - AppsFailed=$.AppsFailed + - AppsPending=$.AppsPending + - AppsRunning=$.AppsRunning + - AppsSubmitted=$.AppsSubmitted + - running_0=$.running_0 + - running_60=$.running_60 + - running_300=$.running_300 + - running_1440=$.running_1440 + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name =~ /Hadoop:service=ResourceManager,name=QueueMetrics.*/)]' + + - name: runtime + i18n: + zh-CN: 运行时信息 + en-US: Runtime + ja-JP: ランタイム情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 3 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: StartTime + type: 0 + i18n: + zh-CN: 启动时间戳 + en-US: StartTime + ja-JP: 起動時間 + # (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field + aliasFields: + - $.StartTime + calculates: + - StartTime=$.StartTime + protocol: http + http: + host: ^_^host^_^ + port: ^_^port^_^ + url: /jmx + method: GET + ssl: ^_^ssl^_^ + parseType: jsonPath + parseScript: '$.beans[?(@.name == "java.lang:type=Runtime")]' diff --git a/hertzbeat-manager/src/main/resources/define/app-zookeeper.yml b/hertzbeat-manager/src/main/resources/define/app-zookeeper.yml new file mode 100644 index 0000000..c84568e --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-zookeeper.yml @@ -0,0 +1,417 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring +category: mid +# The monitoring type eg: linux windows tomcat mysql aws... +app: zookeeper +# The monitoring i18n name +name: + zh-CN: Zookeeper服务 + en-US: Zookeeper Server + ja-JP: Zookeeperサーバー +# The description and help of this monitoring type +help: + zh-CN: HertzBeat 支持对 Zookeeper 的通用性能指标(conf、stats)进行采集监控。<br><span class='help_module_span'>注意⚠️:由于需要 zookeeper 提供的四字命令 + netcat 来收集指标数据,您需要将 zookeeper 的四字命令加入白名单,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/zookeeper'>点击查看具体步骤</a>。</span> + en-US: HertzBeat monitors Zookeeper general performance metrics such as status and configuration. <br><span class='help_module_span'>Note⚠️:Due to the need for four-letter-word provided by zookeeper to collect metric data, you should add the four-letter-word to the whitelist.<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/zookeeper'>Click here to view the specific steps.</a></span> + zh-TW: HertzBeat 支持對 Zookeeper 的通用性能指標(conf、stats)進行采集監控。<br><span class='help_module_span'>注意⚠️:由于需要 zookeeper 提供的四字命令 + netcat 來收集指標數據,您需要將 zookeeper 的四字命令加入白名單,<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/zookeeper'>點擊查看具體步驟</a>。</span> + ja-JP: HertzBeat はZookeeperの一般的なパフォーマンスメトリクス(conf、stats)の収集・監視をサポートしています <br><span class='help_module_span'>注意⚠️:Zookeeper が提供する4文字のコマンドと netcat を使用してメトリックデータを収集する必要があるため、Zookeeper の4文字のコマンドをホワイトリストに追加する必要があります、<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/zookeeper'>詳細な手順はこちらをクリックしてご覧ください</a></span> +helpLink: + zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/zookeeper + en-US: https://hertzbeat.apache.org/docs/help/zookeeper +# Input params define for monitoring(render web ui by the definition) +params: + # field-param field key + - field: host + # name-param field display i18n name + name: + zh-CN: 目标Host + en-US: Target Host + ja-JP: 目標ホスト + # type-param field type(most mapping the html input type) + type: host + # required-true or false + required: true + # field-param field key + - field: port + # name-param field display i18n name + name: + zh-CN: 端口 + en-US: Port + ja-JP: ポート + # type-param field type(most mapping the html input type) + type: number + # when type is number, range is required + range: '[0,65535]' + # required-true or false + required: true + # default value + defaultValue: 2181 + # field-param field key + - field: timeout + # name-param field display i18n name + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + # type-param field type(most mapping the html input type) + type: number + # required-true or false + required: false + # hide-is hide this field and put it in advanced layout + hide: true + # default value + defaultValue: 6000 +# collect metrics config list +metrics: + # metrics - cluster + - name: conf + i18n: + zh-CN: 配置信息 + en-US: Configuration Info + ja-JP: 配置情報 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: clientPort + type: 0 + i18n: + zh-CN: 客户端端口 + en-US: Client Port + ja-JP: クライエントポート + - field: dataDir + type: 1 + i18n: + zh-CN: 数据目录 + en-US: Data Directory + ja-JP: データディレクトリ + - field: dataDirSize + type: 0 + unit: kb + i18n: + zh-CN: 数据目录大小 + en-US: Data Directory Size + ja-JP: データディレクトリのサイズ + - field: dataLogDir + type: 1 + i18n: + zh-CN: 日志目录 + en-US: Data Log Directory + ja-JP: データログのディレクトリ + - field: dataLogSize + type: 0 + unit: kb + i18n: + zh-CN: 日志目录大小 + en-US: Data Log Size + ja-JP: データログのディレクトリのサイズ + - field: tickTime + type: 0 + unit: ms + i18n: + zh-CN: 心跳间隔时间 + en-US: Tick Time + ja-JP: ハートビート間隔時間 + - field: maxClientCnxns + type: 1 + i18n: + zh-CN: 最大客户端连接数 + en-US: Max Client Connections + ja-JP: 最大クライエント接続数 + - field: minSessionTimeout + type: 0 + unit: ms + i18n: + zh-CN: 最小会话超时 + en-US: Min Session Timeout + ja-JP: セッションタイムアウトの最小値 + - field: maxSessionTimeout + type: 0 + unit: ms + i18n: + zh-CN: 最大会话超时 + en-US: Max Session Timeout + ja-JP: セッションタイムアウトの最大値 + - field: serverId + type: 0 + i18n: + zh-CN: 服务器ID + en-US: Server ID + ja-JP: サーバーID + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: telnet + telnet: + # telnet host: ipv4 ipv6 domain + host: ^_^host^_^ + # telnet port: 22 + port: ^_^port^_^ + # telnet connect timeout + timeout: ^_^timeout^_^ + # telnet collect cmd + cmd: conf + - name: stats + i18n: + zh-CN: 统计信息 + en-US: Stats + ja-JP: 統計情報 + priority: 1 + fields: + - field: zk_version + type: 1 + i18n: + zh-CN: ZooKeeper版本 + en-US: ZooKeeper Version + ja-JP: ZooKeeperのバージョン + - field: zk_server_state + type: 1 + i18n: + zh-CN: 服务器状态 + en-US: Server State + ja-JP: サーバーの状態 + - field: zk_num_alive_connections + type: 0 + unit: 个 + i18n: + zh-CN: 存活连接数 + en-US: Number of Alive Connections + ja-JP: アライブ接続数 + - field: zk_avg_latency + type: 0 + unit: ms + i18n: + zh-CN: 平均延迟 + en-US: Average Latency + ja-JP: 平均遅延 + - field: zk_outstanding_requests + type: 0 + unit: 个 + i18n: + zh-CN: 未完成请求数 + en-US: Outstanding Requests + ja-JP: 未完了のリクエスト数 + - field: zk_znode_count + type: 0 + unit: 个 + i18n: + zh-CN: ZNode数量 + en-US: ZNode Count + ja-JP: ZNodeの数 + - field: zk_packets_sent + type: 0 + unit: 个 + i18n: + zh-CN: 发送数据包数 + en-US: Packets Sent + ja-JP: 転送されたパケットの数 + - field: zk_packets_received + type: 0 + unit: 个 + i18n: + zh-CN: 接收数据包数 + en-US: Packets Received + ja-JP: 受信されたパケットの数 + - field: zk_watch_count + type: 0 + unit: 个 + i18n: + zh-CN: Watch数量 + en-US: Watch Count + ja-JP: Watchの数 + - field: zk_max_file_descriptor_count + type: 0 + unit: 个 + i18n: + zh-CN: 最大文件描述符数量 + en-US: Max File Descriptor Count + ja-JP: 開かれたファイル記述子の最大値 + - field: zk_approximate_data_size + type: 0 + unit: kb + i18n: + zh-CN: 大致数据大小 + en-US: Approximate Data Size + ja-JP: 概算データサイズ + - field: zk_open_file_descriptor_count + type: 0 + unit: 个 + i18n: + zh-CN: 打开的文件描述符数量 + en-US: Open File Descriptor Count + ja-JP: 開かれたファイル記述子の数 + - field: zk_max_latency + type: 0 + unit: ms + i18n: + zh-CN: 最大延迟 + en-US: Max Latency + ja-JP: 最大遅延 + - field: zk_ephemerals_count + type: 0 + unit: 个 + i18n: + zh-CN: 临时节点数量 + en-US: Ephemerals Count + ja-JP: 一時的なノードの数 + - field: zk_min_latency + type: 0 + unit: ms + i18n: + zh-CN: 最小延迟 + en-US: Min Latency + ja-JP: 最小遅延 + protocol: telnet + telnet: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + cmd: mntr + - name: envi + i18n: + zh-CN: 环境信息 + en-US: Environment + ja-JP: 環境情報 + priority: 2 + fields: + - field: zk_version + type: 1 + i18n: + zh-CN: ZooKeeper版本 + en-US: ZooKeeper Version + ja-JP: ZooKeeperのバージョン + - field: hostname + type: 1 + i18n: + zh-CN: 主机名 + en-US: Host Name + ja-JP: ホスト名 + - field: java_version + type: 1 + i18n: + zh-CN: Java版本 + en-US: Java Version + ja-JP: Javaのバージョン + - field: java_vendor + type: 1 + i18n: + zh-CN: Java供应商 + en-US: Java Vendor + ja-JP: Javaのベンダー + - field: java_home + type: 1 + i18n: + zh-CN: Java主目录 + en-US: Java Home + ja-JP: Javaのホームディレクトリ + - field: java_class_path + type: 1 + i18n: + zh-CN: Java类路径 + en-US: Java Class Path + ja-JP: Javaのクラスパス + - field: java_library_path + type: 1 + i18n: + zh-CN: Java库路径 + en-US: Java Library Path + ja-JP: Javaのライブラリパス + - field: java_io_tmpdir + type: 1 + i18n: + zh-CN: Java临时目录 + en-US: Java IO Temp Directory + ja-JP: Java IOの一時的なディレクトリ + - field: java_compiler + type: 1 + i18n: + zh-CN: Java编译器 + en-US: Java Compiler + ja-JP: Javaのコンパイラ + - field: os_name + type: 1 + i18n: + zh-CN: 操作系统名称 + en-US: OS Name + ja-JP: オーエスの名前 + - field: os_arch + type: 1 + i18n: + zh-CN: 操作系统架构 + en-US: OS Architecture + ja-JP: オーエスアーキテクチャ + - field: os_version + type: 1 + i18n: + zh-CN: 操作系统版本 + en-US: OS Version + ja-JP: オーエスバージョン + - field: user_name + type: 1 + i18n: + zh-CN: 用户名 + en-US: User Name + ja-JP: ユーザー名 + - field: user_home + type: 1 + i18n: + zh-CN: 用户主目录 + en-US: User Home + ja-JP: ユーザーのホームディレクトリ + - field: user_dir + type: 1 + i18n: + zh-CN: 用户当前目录 + en-US: User Directory + ja-JP: ユーザーの現在のディレクトリ + aliasFields: + - zookeeper.version + - host.name + - java.version + - java.vendor + - user.dir + - java.home + - java.class.path + - java.library.path + - java.io.tmpdir + - java.compiler + - os.name + - os.arch + - os.version + - user.name + - user.home + calculates: + - zk_version=zookeeper.version + - hostname=host.name + - java_version=java.version + - java_vendor=java.vendor + - user_dir=user.dir + - java_home=java.home + - java_class_path=java.class.path + - java_library_path=java.library.path + - java_io_tmpdir=java.io.tmpdir + - java_compiler=java.compiler + - os_name=os.name + - os_arch=os.arch + - os_version=os.version + - user_name=user.name + - user_home=user.home + protocol: telnet + telnet: + host: ^_^host^_^ + port: ^_^port^_^ + timeout: ^_^timeout^_^ + cmd: envi diff --git a/hertzbeat-manager/src/main/resources/define/app-zookeeper_sd.yml b/hertzbeat-manager/src/main/resources/define/app-zookeeper_sd.yml new file mode 100644 index 0000000..bcba931 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-zookeeper_sd.yml @@ -0,0 +1,76 @@ +# 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. + +# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring +category: __system__ +# Monitoring application type name (consistent with file name) eg: linux windows tomcat mysql aws... +app: zookeeper_sd +# The app api i18n name +name: + zh-CN: Zookeeper Service Discovery + en-US: Zookeeper Service Discovery + ja-JP: Zookeeper サービスディスカバリー +# Input params define for app api(render web ui by the definition) +params: + # field-param field key + - field: __sd_url__ + # name-param field display i18n name + name: + zh-CN: 服务发现地址 + en-US: Service Discovery Url + ja-JP: サービスディスカバリー Url + # type-param field type(most mapping the html input type) + type: text + # required-true or false + required: true + + - field: __sd_path_prefix__ + name: + zh-CN: 服务发现路径前缀 + en-US: Service Discovery Path Prefix + ja-JP: サービスディスカバリーのパスプレフィックス + type: text + required: true + +metrics: + - name: target + i18n: + zh-CN: 监控目标 + en-US: Monitor Target + ja-JP: 監視対象 + # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel + # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue + priority: 0 + # collect metrics content + fields: + # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field + - field: host + type: 1 + i18n: + zh-CN: Host + en-US: Host + ja-JP: ホスト + - field: port + type: 1 + i18n: + zh-CN: Port + en-US: Port + ja-JP: ポート + # the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk + protocol: zookeeper_sd + # the config content when protocol is zookeeper_sd + zookeeper_sd: + url: ^_^__sd_url__^_^ + pathPrefix: ^_^__sd_path_prefix__^_^ diff --git a/hertzbeat-manager/src/main/resources/grafana/hertzbeat-monitor-zh-CN.json b/hertzbeat-manager/src/main/resources/grafana/hertzbeat-monitor-zh-CN.json new file mode 100644 index 0000000..27e73c2 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/grafana/hertzbeat-monitor-zh-CN.json @@ -0,0 +1,1083 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "panel", + "id": "bargauge", + "name": "Bar gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "12.0.0" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)" + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 1 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 5 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "失败任务数" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "成功任务数" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#73BF69", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "总监控任务数" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#5794F2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "count(group by (monitor_id)(hertzbeat_collect_total))", + "interval": "", + "legendFormat": "总监控任务数", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "count(group by (monitor_id)(hertzbeat_collect_total{status=\"success\"}))", + "interval": "", + "legendFormat": "成功任务数", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "count(group by (monitor_id)(hertzbeat_collect_total{status=\"fail\"}))", + "interval": "", + "legendFormat": "失败任务数", + "refId": "C" + } + ], + "title": "监控任务状态概览", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#73BF69" + }, + { + "color": "#FADE2A", + "value": 10 + }, + { + "color": "#FF9830", + "value": 25 + }, + { + "color": "#F2495C", + "value": 50 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "displayMode": "lcd", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "text": {}, + "valueMode": "color" + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "(\n count(group by (monitor_id)(hertzbeat_collect_total{status=\"fail\"}))\n / \n count(group by (monitor_id)(hertzbeat_collect_total))\n) * 100", + "interval": "", + "legendFormat": "任务失败率", + "refId": "A" + } + ], + "title": "任务失败率", + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#73BF69" + }, + { + "color": "#FADE2A", + "value": 5 + }, + { + "color": "#FF9830", + "value": 15 + }, + { + "color": "#F2495C", + "value": 30 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 12, + "y": 4 + }, + "id": 10, + "options": { + "displayMode": "lcd", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "text": {}, + "valueMode": "color" + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "clamp_max((\n sum(rate(hertzbeat_collect_total{status=\"fail\"}[5m]))\n / \n sum(rate(hertzbeat_collect_total[5m]))\n) * 100, 100)", + "interval": "", + "legendFormat": "采集失败率", + "refId": "A" + } + ], + "title": "实时采集失败率", + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "监控名称" + }, + "properties": [ + { + "id": "custom.width", + "value": 200 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "监控类型" + }, + "properties": [ + { + "id": "custom.width", + "value": 150 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "失败次数" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + }, + { + "id": "color", + "value": { + "mode": "fixed", + "seriesBy": "last" + } + }, + { + "id": "max", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "平均耗时" + }, + "properties": [ + { + "id": "unit", + "value": "ms" + }, + { + "id": "decimals", + "value": 1 + }, + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + }, + { + "id": "max", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "最近失败时间" + }, + "properties": [ + { + "id": "unit", + "value": "time:YYYY-MM-DD HH:mm:ss" + } + ] + } + ] + }, + "gridPos": { + "h": 15, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 9, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "最近失败时间" + } + ] + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum by (monitor_name, monitor_type, monitor_target) (hertzbeat_collect_total{status=\"success\"})", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum by (monitor_name, monitor_type, monitor_target) (hertzbeat_collect_total{status=\"fail\"})", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum by (monitor_name, monitor_type, monitor_target) ((hertzbeat_collect_duration_seconds_sum / hertzbeat_collect_duration_seconds_count) * 1000)", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "max by (monitor_name) (timestamp(hertzbeat_collect_total{status=\"fail\"}))", + "format": "table", + "instant": true, + "refId": "D" + } + ], + "title": "监控任务详情 (按最近失败时间排序)", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true, + "application": true, + "environment": true, + "instance": true, + "job": true, + "monitor_id": true + }, + "indexByName": {}, + "renameByName": { + "Value #A": "成功次数", + "Value #B": "失败次数", + "Value #C": "平均耗时", + "Value #D": "最近失败时间", + "monitor_name": "监控名称", + "monitor_target": "目标地址", + "monitor_type": "监控类型" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": ".*fail.*" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": ".*success.*" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#73BF69", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 4, + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(rate(hertzbeat_collect_total[1m]) * 60) by (monitor_name, status)", + "interval": "", + "legendFormat": "{{monitor_name}} - {{status}}", + "refId": "A" + } + ], + "title": "采集任务执行频率 (每分钟)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "成功采集次数", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 11, + "options": { + "legend": { + "calcs": [ + "last", + "max", + "diff" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "hertzbeat_collect_total{status=\"success\"}", + "interval": "", + "legendFormat": "{{monitor_name}} - 成功次数", + "refId": "A" + } + ], + "title": "累积成功次数趋势", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "耗时 (毫秒)", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 100 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 6, + "options": { + "legend": { + "calcs": [ + "mean", + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "(hertzbeat_collect_duration_seconds_sum / hertzbeat_collect_duration_seconds_count) * 1000", + "interval": "", + "legendFormat": "{{monitor_name}} ({{monitor_type}})", + "refId": "A" + } + ], + "title": "平均采集耗时趋势", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "累积失败次数", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 7, + "options": { + "legend": { + "calcs": [ + "last", + "max", + "diff" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "hertzbeat_collect_total{status=\"fail\"}", + "interval": "", + "legendFormat": "{{monitor_name}} - 失败次数", + "refId": "A" + } + ], + "title": "累积失败次数趋势", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 41, + "tags": [ + "hertzbeat", + "monitoring", + "prometheus" + ], + "templating": { + "list": [ + { + "current": {}, + "includeAll": false, + "label": "数据源", + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "HertzBeat Monitoring Dashboard", + "uid": "hertzbeat-monitoring-complete-revised", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/hertzbeat-manager/src/main/resources/templates/1-EmailTemplate.html b/hertzbeat-manager/src/main/resources/templates/1-EmailTemplate.html new file mode 100644 index 0000000..85a927d --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/1-EmailTemplate.html @@ -0,0 +1,112 @@ +<!-- + ~ 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. +--> + +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en"> +<head> + <meta charset="UTF-8"> + <title>HertzBeat Alert Notification + + + +
+
+

🔔 HertzBeat Alert Notification

+
+ +
+

Alert Summary

+
Status: ${status!"UNKNOWN"}
+ <#if commonLabels??> + <#if commonLabels.severity??> + <#assign severityClass = commonLabels.severity?switch( + "critical", "severity-critical", + "warning", "severity-warning", + "info", "severity-info", + "")> +
Severity: ${commonLabels.severity?switch( + "critical", "❤️ Critical", + "warning", "💛 Warning", + "info", "💚 Info", + "Unknown")}
+ + <#if commonLabels.alertname??> +
Alert Name: ${commonLabels.alertname?html}
+ + +
+ +
+

Alert Details

+ <#if alerts?? && alerts?size gt 0> + <#list alerts as alert> +
+

Alert ${alert?index + 1}

+ <#if alert.labels?? && alert.labels?size gt 0> + <#list alert.labels?keys as key> +
${key?html}: ${alert.labels[key]?html}
+ + + <#if alert.content?? && alert.content != ""> +
Content: ${alert.content?html}
+ +
Trigger Count: ${alert.triggerTimes!0}
+
Start Time: ${((alert.startAt!0)?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')}
+ <#if alert.activeAt?? && alert.activeAt gt 0> +
Active Time: ${((alert.activeAt!0)?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')}
+ + <#if alert.endAt?? && alert.endAt gt 0> +
End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')}
+ + + <#if alert.annotations?? && alert.annotations?size gt 0> +

Additional Information

+ <#list alert.annotations?keys as key> +
${key?html}: ${alert.annotations[key]?html}
+ + +
+ + +
+ + <#if commonAnnotations?? && commonAnnotations?size gt 0> +
+

Common Information

+ <#list commonAnnotations?keys as key> +
${key?html}: ${commonAnnotations[key]?html}
+ +
+ +
+ + diff --git a/hertzbeat-manager/src/main/resources/templates/10-WeWorkAppTemplate.txt b/hertzbeat-manager/src/main/resources/templates/10-WeWorkAppTemplate.txt new file mode 100644 index 0000000..4f9e8de --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/10-WeWorkAppTemplate.txt @@ -0,0 +1,47 @@ +## 🔔 HertzBeat Alert Notification + +### Alert Summary +> - Status: ${status} +<#if commonLabels??> +<#if commonLabels.severity??> +> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")} + +<#if commonLabels.alertname??> +> - Alert Name: ${commonLabels.alertname} + + + +### Alert Details +<#list alerts as alert> +#### Alert ${alert?index + 1} +<#if alert.labels??> +<#list alert.labels?keys as key> +> - ${key}: ${alert.labels[key]} + + +<#if alert.content?? && alert.content != ""> +> - Content: ${alert.content} + +> - Trigger Count: ${alert.triggerTimes} +> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} +<#if alert.activeAt??> +> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + +<#if alert.endAt??> +> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + + +<#if alert.annotations?? && alert.annotations?size gt 0> +##### Additional Information +<#list alert.annotations?keys as key> +> - ${key}: ${alert.annotations[key]} + + + + +<#if commonAnnotations?? && commonAnnotations?size gt 0> +### Common Information +<#list commonAnnotations?keys as key> +> - ${key}: ${commonAnnotations[key]} + + diff --git a/hertzbeat-manager/src/main/resources/templates/11-HuaweiCloudSmnTemplate.txt b/hertzbeat-manager/src/main/resources/templates/11-HuaweiCloudSmnTemplate.txt new file mode 100644 index 0000000..4f9e8de --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/11-HuaweiCloudSmnTemplate.txt @@ -0,0 +1,47 @@ +## 🔔 HertzBeat Alert Notification + +### Alert Summary +> - Status: ${status} +<#if commonLabels??> +<#if commonLabels.severity??> +> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")} + +<#if commonLabels.alertname??> +> - Alert Name: ${commonLabels.alertname} + + + +### Alert Details +<#list alerts as alert> +#### Alert ${alert?index + 1} +<#if alert.labels??> +<#list alert.labels?keys as key> +> - ${key}: ${alert.labels[key]} + + +<#if alert.content?? && alert.content != ""> +> - Content: ${alert.content} + +> - Trigger Count: ${alert.triggerTimes} +> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} +<#if alert.activeAt??> +> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + +<#if alert.endAt??> +> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + + +<#if alert.annotations?? && alert.annotations?size gt 0> +##### Additional Information +<#list alert.annotations?keys as key> +> - ${key}: ${alert.annotations[key]} + + + + +<#if commonAnnotations?? && commonAnnotations?size gt 0> +### Common Information +<#list commonAnnotations?keys as key> +> - ${key}: ${commonAnnotations[key]} + + diff --git a/hertzbeat-manager/src/main/resources/templates/12-ServerChanTemplate.txt b/hertzbeat-manager/src/main/resources/templates/12-ServerChanTemplate.txt new file mode 100644 index 0000000..4f9e8de --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/12-ServerChanTemplate.txt @@ -0,0 +1,47 @@ +## 🔔 HertzBeat Alert Notification + +### Alert Summary +> - Status: ${status} +<#if commonLabels??> +<#if commonLabels.severity??> +> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")} + +<#if commonLabels.alertname??> +> - Alert Name: ${commonLabels.alertname} + + + +### Alert Details +<#list alerts as alert> +#### Alert ${alert?index + 1} +<#if alert.labels??> +<#list alert.labels?keys as key> +> - ${key}: ${alert.labels[key]} + + +<#if alert.content?? && alert.content != ""> +> - Content: ${alert.content} + +> - Trigger Count: ${alert.triggerTimes} +> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} +<#if alert.activeAt??> +> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + +<#if alert.endAt??> +> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + + +<#if alert.annotations?? && alert.annotations?size gt 0> +##### Additional Information +<#list alert.annotations?keys as key> +> - ${key}: ${alert.annotations[key]} + + + + +<#if commonAnnotations?? && commonAnnotations?size gt 0> +### Common Information +<#list commonAnnotations?keys as key> +> - ${key}: ${commonAnnotations[key]} + + diff --git a/hertzbeat-manager/src/main/resources/templates/13-GotifyTemplate.txt b/hertzbeat-manager/src/main/resources/templates/13-GotifyTemplate.txt new file mode 100644 index 0000000..4f9e8de --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/13-GotifyTemplate.txt @@ -0,0 +1,47 @@ +## 🔔 HertzBeat Alert Notification + +### Alert Summary +> - Status: ${status} +<#if commonLabels??> +<#if commonLabels.severity??> +> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")} + +<#if commonLabels.alertname??> +> - Alert Name: ${commonLabels.alertname} + + + +### Alert Details +<#list alerts as alert> +#### Alert ${alert?index + 1} +<#if alert.labels??> +<#list alert.labels?keys as key> +> - ${key}: ${alert.labels[key]} + + +<#if alert.content?? && alert.content != ""> +> - Content: ${alert.content} + +> - Trigger Count: ${alert.triggerTimes} +> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} +<#if alert.activeAt??> +> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + +<#if alert.endAt??> +> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + + +<#if alert.annotations?? && alert.annotations?size gt 0> +##### Additional Information +<#list alert.annotations?keys as key> +> - ${key}: ${alert.annotations[key]} + + + + +<#if commonAnnotations?? && commonAnnotations?size gt 0> +### Common Information +<#list commonAnnotations?keys as key> +> - ${key}: ${commonAnnotations[key]} + + diff --git a/hertzbeat-manager/src/main/resources/templates/14-FeiShuAppTemplate.txt b/hertzbeat-manager/src/main/resources/templates/14-FeiShuAppTemplate.txt new file mode 100644 index 0000000..4f9e8de --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/14-FeiShuAppTemplate.txt @@ -0,0 +1,47 @@ +## 🔔 HertzBeat Alert Notification + +### Alert Summary +> - Status: ${status} +<#if commonLabels??> +<#if commonLabels.severity??> +> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")} + +<#if commonLabels.alertname??> +> - Alert Name: ${commonLabels.alertname} + + + +### Alert Details +<#list alerts as alert> +#### Alert ${alert?index + 1} +<#if alert.labels??> +<#list alert.labels?keys as key> +> - ${key}: ${alert.labels[key]} + + +<#if alert.content?? && alert.content != ""> +> - Content: ${alert.content} + +> - Trigger Count: ${alert.triggerTimes} +> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} +<#if alert.activeAt??> +> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + +<#if alert.endAt??> +> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + + +<#if alert.annotations?? && alert.annotations?size gt 0> +##### Additional Information +<#list alert.annotations?keys as key> +> - ${key}: ${alert.annotations[key]} + + + + +<#if commonAnnotations?? && commonAnnotations?size gt 0> +### Common Information +<#list commonAnnotations?keys as key> +> - ${key}: ${commonAnnotations[key]} + + diff --git a/hertzbeat-manager/src/main/resources/templates/2-WebhookTemplate.txt b/hertzbeat-manager/src/main/resources/templates/2-WebhookTemplate.txt new file mode 100644 index 0000000..163084a --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/2-WebhookTemplate.txt @@ -0,0 +1,53 @@ +{ + "title": "🔔 HertzBeat Alert Notification", + "status": "${status!"UNKNOWN"}", + "commonLabels": { + <#if commonLabels?? && commonLabels.severity??> + "severity": "${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")}"<#if commonLabels.alertname??>, + + <#if commonLabels?? && commonLabels.alertname??> + "alertName": "${commonLabels.alertname}" + + }, + "alerts": [ + <#if alerts?? && alerts?size gt 0> + <#list alerts as alert> + { + "index": ${alert?index + 1}, + "labels": { + <#if alert.labels?? && alert.labels?size gt 0> + <#list alert.labels?keys as key> + "${key}": "${alert.labels[key]?json_string}"<#if key?has_next>, + + + }, + <#if alert.content?? && alert.content != ""> + "content": "${alert.content?json_string}", + + "triggerTimes": ${alert.triggerTimes!0}, + "startAt": "${((alert.startAt!0)?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')}", + <#if alert.activeAt?? && alert.activeAt gt 0> + "activeAt": "${((alert.activeAt!0)?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')}", + + <#if alert.endAt?? && alert.endAt gt 0> + "endAt": "${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')}"<#if alert.annotations?? && alert.annotations?size gt 0>, + + <#if alert.annotations?? && alert.annotations?size gt 0> + "annotations": { + <#list alert.annotations?keys as key> + "${key}": "${alert.annotations[key]?json_string}"<#if key?has_next>, + + } + + }<#if alert?has_next>, + + + ], + "commonAnnotations": { + <#if commonAnnotations?? && commonAnnotations?size gt 0> + <#list commonAnnotations?keys as key> + "${key}": "${commonAnnotations[key]?json_string}"<#if key?has_next>, + + + } +} diff --git a/hertzbeat-manager/src/main/resources/templates/4-WeWorkRobotTemplate.txt b/hertzbeat-manager/src/main/resources/templates/4-WeWorkRobotTemplate.txt new file mode 100644 index 0000000..4f9e8de --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/4-WeWorkRobotTemplate.txt @@ -0,0 +1,47 @@ +## 🔔 HertzBeat Alert Notification + +### Alert Summary +> - Status: ${status} +<#if commonLabels??> +<#if commonLabels.severity??> +> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")} + +<#if commonLabels.alertname??> +> - Alert Name: ${commonLabels.alertname} + + + +### Alert Details +<#list alerts as alert> +#### Alert ${alert?index + 1} +<#if alert.labels??> +<#list alert.labels?keys as key> +> - ${key}: ${alert.labels[key]} + + +<#if alert.content?? && alert.content != ""> +> - Content: ${alert.content} + +> - Trigger Count: ${alert.triggerTimes} +> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} +<#if alert.activeAt??> +> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + +<#if alert.endAt??> +> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + + +<#if alert.annotations?? && alert.annotations?size gt 0> +##### Additional Information +<#list alert.annotations?keys as key> +> - ${key}: ${alert.annotations[key]} + + + + +<#if commonAnnotations?? && commonAnnotations?size gt 0> +### Common Information +<#list commonAnnotations?keys as key> +> - ${key}: ${commonAnnotations[key]} + + diff --git a/hertzbeat-manager/src/main/resources/templates/5-DingTalkRobotTemplate.txt b/hertzbeat-manager/src/main/resources/templates/5-DingTalkRobotTemplate.txt new file mode 100644 index 0000000..4f9e8de --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/5-DingTalkRobotTemplate.txt @@ -0,0 +1,47 @@ +## 🔔 HertzBeat Alert Notification + +### Alert Summary +> - Status: ${status} +<#if commonLabels??> +<#if commonLabels.severity??> +> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")} + +<#if commonLabels.alertname??> +> - Alert Name: ${commonLabels.alertname} + + + +### Alert Details +<#list alerts as alert> +#### Alert ${alert?index + 1} +<#if alert.labels??> +<#list alert.labels?keys as key> +> - ${key}: ${alert.labels[key]} + + +<#if alert.content?? && alert.content != ""> +> - Content: ${alert.content} + +> - Trigger Count: ${alert.triggerTimes} +> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} +<#if alert.activeAt??> +> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + +<#if alert.endAt??> +> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + + +<#if alert.annotations?? && alert.annotations?size gt 0> +##### Additional Information +<#list alert.annotations?keys as key> +> - ${key}: ${alert.annotations[key]} + + + + +<#if commonAnnotations?? && commonAnnotations?size gt 0> +### Common Information +<#list commonAnnotations?keys as key> +> - ${key}: ${commonAnnotations[key]} + + diff --git a/hertzbeat-manager/src/main/resources/templates/6-FlyBookRobotTemplate.txt b/hertzbeat-manager/src/main/resources/templates/6-FlyBookRobotTemplate.txt new file mode 100644 index 0000000..4f9e8de --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/6-FlyBookRobotTemplate.txt @@ -0,0 +1,47 @@ +## 🔔 HertzBeat Alert Notification + +### Alert Summary +> - Status: ${status} +<#if commonLabels??> +<#if commonLabels.severity??> +> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")} + +<#if commonLabels.alertname??> +> - Alert Name: ${commonLabels.alertname} + + + +### Alert Details +<#list alerts as alert> +#### Alert ${alert?index + 1} +<#if alert.labels??> +<#list alert.labels?keys as key> +> - ${key}: ${alert.labels[key]} + + +<#if alert.content?? && alert.content != ""> +> - Content: ${alert.content} + +> - Trigger Count: ${alert.triggerTimes} +> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} +<#if alert.activeAt??> +> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + +<#if alert.endAt??> +> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + + +<#if alert.annotations?? && alert.annotations?size gt 0> +##### Additional Information +<#list alert.annotations?keys as key> +> - ${key}: ${alert.annotations[key]} + + + + +<#if commonAnnotations?? && commonAnnotations?size gt 0> +### Common Information +<#list commonAnnotations?keys as key> +> - ${key}: ${commonAnnotations[key]} + + diff --git a/hertzbeat-manager/src/main/resources/templates/7-TelegramBotTemplate.txt b/hertzbeat-manager/src/main/resources/templates/7-TelegramBotTemplate.txt new file mode 100644 index 0000000..4f9e8de --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/7-TelegramBotTemplate.txt @@ -0,0 +1,47 @@ +## 🔔 HertzBeat Alert Notification + +### Alert Summary +> - Status: ${status} +<#if commonLabels??> +<#if commonLabels.severity??> +> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")} + +<#if commonLabels.alertname??> +> - Alert Name: ${commonLabels.alertname} + + + +### Alert Details +<#list alerts as alert> +#### Alert ${alert?index + 1} +<#if alert.labels??> +<#list alert.labels?keys as key> +> - ${key}: ${alert.labels[key]} + + +<#if alert.content?? && alert.content != ""> +> - Content: ${alert.content} + +> - Trigger Count: ${alert.triggerTimes} +> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} +<#if alert.activeAt??> +> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + +<#if alert.endAt??> +> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + + +<#if alert.annotations?? && alert.annotations?size gt 0> +##### Additional Information +<#list alert.annotations?keys as key> +> - ${key}: ${alert.annotations[key]} + + + + +<#if commonAnnotations?? && commonAnnotations?size gt 0> +### Common Information +<#list commonAnnotations?keys as key> +> - ${key}: ${commonAnnotations[key]} + + diff --git a/hertzbeat-manager/src/main/resources/templates/8-SlackTemplate.txt b/hertzbeat-manager/src/main/resources/templates/8-SlackTemplate.txt new file mode 100644 index 0000000..4f9e8de --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/8-SlackTemplate.txt @@ -0,0 +1,47 @@ +## 🔔 HertzBeat Alert Notification + +### Alert Summary +> - Status: ${status} +<#if commonLabels??> +<#if commonLabels.severity??> +> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")} + +<#if commonLabels.alertname??> +> - Alert Name: ${commonLabels.alertname} + + + +### Alert Details +<#list alerts as alert> +#### Alert ${alert?index + 1} +<#if alert.labels??> +<#list alert.labels?keys as key> +> - ${key}: ${alert.labels[key]} + + +<#if alert.content?? && alert.content != ""> +> - Content: ${alert.content} + +> - Trigger Count: ${alert.triggerTimes} +> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} +<#if alert.activeAt??> +> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + +<#if alert.endAt??> +> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + + +<#if alert.annotations?? && alert.annotations?size gt 0> +##### Additional Information +<#list alert.annotations?keys as key> +> - ${key}: ${alert.annotations[key]} + + + + +<#if commonAnnotations?? && commonAnnotations?size gt 0> +### Common Information +<#list commonAnnotations?keys as key> +> - ${key}: ${commonAnnotations[key]} + + diff --git a/hertzbeat-manager/src/main/resources/templates/9-DiscordBotTemplate.txt b/hertzbeat-manager/src/main/resources/templates/9-DiscordBotTemplate.txt new file mode 100644 index 0000000..4f9e8de --- /dev/null +++ b/hertzbeat-manager/src/main/resources/templates/9-DiscordBotTemplate.txt @@ -0,0 +1,47 @@ +## 🔔 HertzBeat Alert Notification + +### Alert Summary +> - Status: ${status} +<#if commonLabels??> +<#if commonLabels.severity??> +> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")} + +<#if commonLabels.alertname??> +> - Alert Name: ${commonLabels.alertname} + + + +### Alert Details +<#list alerts as alert> +#### Alert ${alert?index + 1} +<#if alert.labels??> +<#list alert.labels?keys as key> +> - ${key}: ${alert.labels[key]} + + +<#if alert.content?? && alert.content != ""> +> - Content: ${alert.content} + +> - Trigger Count: ${alert.triggerTimes} +> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} +<#if alert.activeAt??> +> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + +<#if alert.endAt??> +> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')} + + +<#if alert.annotations?? && alert.annotations?size gt 0> +##### Additional Information +<#list alert.annotations?keys as key> +> - ${key}: ${alert.annotations[key]} + + + + +<#if commonAnnotations?? && commonAnnotations?size gt 0> +### Common Information +<#list commonAnnotations?keys as key> +> - ${key}: ${commonAnnotations[key]} + + diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/status/CalculateStatusTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/status/CalculateStatusTest.java new file mode 100644 index 0000000..0ec7833 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/status/CalculateStatusTest.java @@ -0,0 +1,148 @@ +/* + * 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.manager.component.status; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; + +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.manager.config.StatusProperties; +import org.apache.hertzbeat.manager.dao.MonitorDao; +import org.apache.hertzbeat.manager.dao.StatusPageComponentDao; +import org.apache.hertzbeat.manager.dao.StatusPageHistoryDao; +import org.apache.hertzbeat.manager.dao.StatusPageOrgDao; +import org.junit.jupiter.api.AfterEach; +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; + +/** + * Test case for {@link CalculateStatus}. + */ +@ExtendWith(MockitoExtension.class) +class CalculateStatusTest { + + @Mock + private StatusPageOrgDao statusPageOrgDao; + + @Mock + private StatusPageComponentDao statusPageComponentDao; + + @Mock + private StatusPageHistoryDao statusPageHistoryDao; + + @Mock + private MonitorDao monitorDao; + + private CalculateStatus calculateStatus; + + @BeforeEach + void setUp() { + calculateStatus = new CalculateStatus(statusPageOrgDao, statusPageComponentDao, statusProperties(), + statusPageHistoryDao, monitorDao, new VirtualThreadProperties(), false); + } + + @AfterEach + void tearDown() { + if (calculateStatus != null) { + calculateStatus.destroy(); + } + } + + @Test + void dispatchCalculateRunsOnVirtualThread() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + org.mockito.Mockito.doAnswer(invocation -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + return Collections.emptyList(); + }).when(statusPageOrgDao).findAll(); + + calculateStatus.dispatchCalculate(); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void dispatchCombineHistoryRunsOnVirtualThread() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + org.mockito.Mockito.doAnswer(invocation -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + return Collections.emptyList(); + }).when(statusPageHistoryDao).findStatusPageHistoriesByTimestampBetween(anyLong(), anyLong()); + + calculateStatus.dispatchCombineHistory(); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void dispatchCalculateDoesNotRunConcurrently() throws Exception { + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + AtomicInteger concurrent = new AtomicInteger(); + AtomicInteger maxConcurrent = new AtomicInteger(); + AtomicInteger invocations = new AtomicInteger(); + org.mockito.Mockito.doAnswer(invocation -> { + int running = concurrent.incrementAndGet(); + maxConcurrent.accumulateAndGet(running, Math::max); + int currentInvocation = invocations.incrementAndGet(); + if (currentInvocation == 1) { + firstStarted.countDown(); + releaseFirst.await(5, TimeUnit.SECONDS); + } else if (currentInvocation == 2) { + secondStarted.countDown(); + } + concurrent.decrementAndGet(); + return Collections.emptyList(); + }).when(statusPageOrgDao).findAll(); + + calculateStatus.dispatchCalculate(); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + calculateStatus.dispatchCalculate(); + assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS)); + + releaseFirst.countDown(); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + assertEquals(1, maxConcurrent.get()); + } + + private StatusProperties statusProperties() { + StatusProperties statusProperties = new StatusProperties(); + StatusProperties.CalculateProperties calculateProperties = new StatusProperties.CalculateProperties(); + calculateProperties.setInterval(300); + statusProperties.setCalculate(calculateProperties); + return statusProperties; + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/ParamValidatorManagerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/ParamValidatorManagerTest.java new file mode 100644 index 0000000..fee8b13 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/ParamValidatorManagerTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.manager.component.validator; + +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +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 java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ParamValidatorManagerTest { + + private ParamValidatorManager paramValidatorManager; + + @Mock + private ParamValidator paramValidator; + + @BeforeEach + void setUp() { + paramValidatorManager = new ParamValidatorManager(List.of(paramValidator)); + } + + @Test + void validate_Success() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("text"); + MonitorParam param = new MonitorParam(); + + when(paramValidator.support("text")).thenReturn(true); + + assertDoesNotThrow(() -> paramValidatorManager.validate(paramDefine, param)); + verify(paramValidator).validate(paramDefine, param); + } + + @Test + void validate_NoValidatorFound() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("unknown"); + MonitorParam param = new MonitorParam(); + + when(paramValidator.support("unknown")).thenReturn(false); + + assertThrows(IllegalArgumentException.class, () -> paramValidatorManager.validate(paramDefine, param)); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/ArrayParamValidatorTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/ArrayParamValidatorTest.java new file mode 100644 index 0000000..dcc1e10 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/ArrayParamValidatorTest.java @@ -0,0 +1,74 @@ +/* + * 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.manager.component.validator.impl; + +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ArrayParamValidatorTest { + + private ArrayParamValidator validator; + + @BeforeEach + void setUp() { + validator = new ArrayParamValidator(); + } + + @Test + void support() { + assertTrue(validator.support("array")); + } + + @Test + void validate_ValidArray() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("array"); + MonitorParam param = new MonitorParam(); + param.setParamValue("val1,val2"); + + assertDoesNotThrow(() -> validator.validate(paramDefine, param)); + } + + @Test + void validate_ValidArrayWithBrackets() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("array"); + MonitorParam param = new MonitorParam(); + param.setParamValue("[val1,val2]"); + + validator.validate(paramDefine, param); + assertEquals("val1,val2", param.getParamValue()); + } + + @Test + void validate_EmptyArray() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("array"); + paramDefine.setField("tags"); + MonitorParam param = new MonitorParam(); + param.setParamValue(""); + assertDoesNotThrow(() -> validator.validate(paramDefine, param)); + assertEquals("", param.getParamValue()); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/BooleanParamValidatorTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/BooleanParamValidatorTest.java new file mode 100644 index 0000000..33e1a64 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/BooleanParamValidatorTest.java @@ -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.manager.component.validator.impl; + +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BooleanParamValidatorTest { + + private BooleanParamValidator validator; + + @BeforeEach + void setUp() { + validator = new BooleanParamValidator(); + } + + @Test + void support() { + assertTrue(validator.support("boolean")); + } + + @Test + void validate_True() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("boolean"); + MonitorParam param = new MonitorParam(); + param.setParamValue("true"); + + assertDoesNotThrow(() -> validator.validate(paramDefine, param)); + } + + @Test + void validate_False() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("boolean"); + MonitorParam param = new MonitorParam(); + param.setParamValue("false"); + + assertDoesNotThrow(() -> validator.validate(paramDefine, param)); + } + + @Test + void validate_CaseInsensitive() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("boolean"); + MonitorParam param = new MonitorParam(); + param.setParamValue("True"); + + assertDoesNotThrow(() -> validator.validate(paramDefine, param)); + } + + @Test + void validate_Invalid() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("boolean"); + paramDefine.setField("ssl"); + MonitorParam param = new MonitorParam(); + param.setParamValue("yes"); + + assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param)); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/HostParamValidatorAdapterTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/HostParamValidatorAdapterTest.java new file mode 100644 index 0000000..bde8aff --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/HostParamValidatorAdapterTest.java @@ -0,0 +1,95 @@ +/* + * 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.manager.component.validator.impl; + +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HostParamValidatorAdapterTest { + + private HostParamValidatorAdapter validator; + + @BeforeEach + void setUp() { + validator = new HostParamValidatorAdapter(); + } + + @Test + void support() { + assertTrue(validator.support("host")); + } + + @Test + void validate_ValidHost() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("host"); + MonitorParam param = new MonitorParam(); + param.setParamValue("127.0.0.1"); + + assertDoesNotThrow(() -> validator.validate(paramDefine, param)); + } + + @Test + void validate_ValidDomain() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("host"); + MonitorParam param = new MonitorParam(); + param.setParamValue("localhost"); + + assertDoesNotThrow(() -> validator.validate(paramDefine, param)); + } + + @Test + void validate_HttpPrefix() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("host"); + MonitorParam param = new MonitorParam(); + param.setParamValue("http://127.0.0.1"); + + assertDoesNotThrow(() -> validator.validate(paramDefine, param)); + } + + @Test + void validate_InvalidHost() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("host"); + paramDefine.setField("host"); + MonitorParam param = new MonitorParam(); + param.setParamValue("invalid host"); + + assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param)); + } + + @Test + void validate_HostWithPort() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("host"); + MonitorParam param = new MonitorParam(); + param.setParamValue("127.0.0.1:8080"); + + assertDoesNotThrow(() -> validator.validate(paramDefine, param)); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/JsonParamValidatorTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/JsonParamValidatorTest.java new file mode 100644 index 0000000..9142897 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/JsonParamValidatorTest.java @@ -0,0 +1,66 @@ +/* + * 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.manager.component.validator.impl; + +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JsonParamValidatorTest { + + private JsonParamValidator validator; + + @BeforeEach + void setUp() { + validator = new JsonParamValidator(); + } + + @Test + void support() { + assertTrue(validator.support("metrics-field")); + assertTrue(validator.support("key-value")); + } + + @Test + void validate_ValidJson() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("key-value"); + MonitorParam param = new MonitorParam(); + param.setParamValue("{\"key\":\"value\"}"); + + assertDoesNotThrow(() -> validator.validate(paramDefine, param)); + } + + @Test + void validate_InvalidJson() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("key-value"); + paramDefine.setField("headers"); + MonitorParam param = new MonitorParam(); + param.setParamValue("{key:value}"); // Invalid JSON + + assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param)); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/NumberParamValidatorTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/NumberParamValidatorTest.java new file mode 100644 index 0000000..ff97082 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/NumberParamValidatorTest.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hertzbeat.manager.component.validator.impl; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class NumberParamValidatorTest { + + private NumberParamValidator validator; + + @BeforeEach + void setUp() { + validator = new NumberParamValidator(); + } + + @Test + void support() { + assertTrue(validator.support("number")); + } + + @Test + void validate_ValidNumber() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("number"); + MonitorParam param = new MonitorParam(); + param.setParamValue("123"); + + validator.validate(paramDefine, param); + assertEquals(CommonConstants.PARAM_TYPE_NUMBER, param.getType()); + } + + @Test + void validate_InvalidNumber() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("number"); + paramDefine.setField("port"); + MonitorParam param = new MonitorParam(); + param.setParamValue("abc"); + + assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param)); + } + + @Test + void validate_NumberInRange() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("number"); + paramDefine.setRange("[0,100]"); + MonitorParam param = new MonitorParam(); + param.setParamValue("50"); + + validator.validate(paramDefine, param); + } + + @Test + void validate_NumberOutOfRange() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("number"); + paramDefine.setField("port"); + paramDefine.setRange("[0,100]"); + MonitorParam param = new MonitorParam(); + param.setParamValue("150"); + + assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param)); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/OptionParamValidatorTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/OptionParamValidatorTest.java new file mode 100644 index 0000000..04cd5ce --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/OptionParamValidatorTest.java @@ -0,0 +1,81 @@ +/* + * 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.manager.component.validator.impl; + +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class OptionParamValidatorTest { + + private OptionParamValidator validator; + + @BeforeEach + void setUp() { + validator = new OptionParamValidator(); + } + + @Test + void support() { + assertTrue(validator.support("radio")); + assertTrue(validator.support("checkbox")); + } + + @Test + void validate_ValidOption() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("radio"); + paramDefine.setOptions(List.of(new ParamDefineInfo.OptionInfo("opt1", "val1"), new ParamDefineInfo.OptionInfo("opt2", "val2"))); + MonitorParam param = new MonitorParam(); + param.setParamValue("val1"); + + assertDoesNotThrow(() -> validator.validate(paramDefine, param)); + } + + @Test + void validate_InvalidOption() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("radio"); + paramDefine.setField("method"); + paramDefine.setOptions(List.of(new ParamDefineInfo.OptionInfo("opt1", "val1"))); + MonitorParam param = new MonitorParam(); + param.setParamValue("val2"); + + assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param)); + } + + @Test + void validate_NullOptions() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("radio"); + paramDefine.setField("method"); + MonitorParam param = new MonitorParam(); + param.setParamValue("val1"); + + assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param)); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidatorTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidatorTest.java new file mode 100644 index 0000000..72e3d21 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidatorTest.java @@ -0,0 +1,75 @@ +/* + * 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.manager.component.validator.impl; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.util.AesUtil; +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PasswordParamValidatorTest { + + private PasswordParamValidator validator; + + @BeforeEach + void setUp() { + validator = new PasswordParamValidator(); + } + + @Test + void support() { + assertTrue(validator.support("password")); + } + + @Test + void validate_EncryptsPlaintext() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("password"); + MonitorParam param = new MonitorParam(); + String plaintext = "password123"; + param.setParamValue(plaintext); + + validator.validate(paramDefine, param); + + assertNotEquals(plaintext, param.getParamValue()); + assertTrue(AesUtil.isCiphertext(param.getParamValue())); + assertEquals(CommonConstants.PARAM_TYPE_PASSWORD, param.getType()); + } + + @Test + void validate_IgnoresCiphertext() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("password"); + MonitorParam param = new MonitorParam(); + String ciphertext = AesUtil.aesEncode("password123"); + param.setParamValue(ciphertext); + + validator.validate(paramDefine, param); + + assertEquals(ciphertext, param.getParamValue()); + assertEquals(CommonConstants.PARAM_TYPE_PASSWORD, param.getType()); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/TextParamValidatorTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/TextParamValidatorTest.java new file mode 100644 index 0000000..e93d64b --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/TextParamValidatorTest.java @@ -0,0 +1,68 @@ +/* + * 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.manager.component.validator.impl; + +import org.apache.hertzbeat.manager.pojo.dto.MonitorParam; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TextParamValidatorTest { + + private TextParamValidator validator; + + @BeforeEach + void setUp() { + validator = new TextParamValidator(); + } + + @Test + void support() { + assertTrue(validator.support("text")); + assertTrue(validator.support("textarea")); + } + + @Test + void validate_ValidLength() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("text"); + paramDefine.setLimit((short) 10); + MonitorParam param = new MonitorParam(); + param.setParamValue("12345"); + + assertDoesNotThrow(() -> validator.validate(paramDefine, param)); + } + + @Test + void validate_OverLimit() { + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setType("text"); + paramDefine.setField("name"); + paramDefine.setLimit((short) 3); + MonitorParam param = new MonitorParam(); + param.setParamValue("12345"); + + assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param)); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java new file mode 100644 index 0000000..36414d8 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java @@ -0,0 +1,263 @@ +/* + * 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.manager.config; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.mockStatic; + +import com.usthe.sureness.subject.PrincipalMap; +import com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.SurenessContextHolder; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; +import org.apache.hertzbeat.common.constants.NetworkConstants; +import org.apache.hertzbeat.manager.service.AccountService; +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; + +/** + * Test case for {@link ApiTokenValidationFilter} + */ +@ExtendWith(MockitoExtension.class) +class ApiTokenValidationFilterTest { + + private ApiTokenValidationFilter filter; + + @Mock + private AccountService accountService; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private PrincipalMap principalMap; + + @BeforeEach + void setUp() { + filter = new ApiTokenValidationFilter(accountService); + } + + @Test + void testNoAuthorizationHeaderPassesThrough() throws Exception { + when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn(null); + SubjectSum subject = mockManagedSubject(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject); + + org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, new Object())); + } + } + + @Test + void testNonBearerAuthorizationPassesThrough() throws Exception { + when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Basic dXNlcjpwYXNz"); + SubjectSum subject = mockManagedSubject(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject); + + org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, new Object())); + } + } + + @Test + void testLegacyTokenPassesThrough() throws Exception { + SubjectSum subject = mockSubject(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject); + + org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, new Object())); + verify(accountService, never()).checkTokenStatus(any()); + } + } + + @Test + void testManagedTokenActivePassesThrough() throws Exception { + String managedToken = "managed-token"; + when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + managedToken); + when(accountService.checkTokenStatus(managedToken)).thenReturn(null); + when(accountService.checkManagedTokenAccess("admin", List.of("admin"))).thenReturn(null); + doNothing().when(accountService).touchTokenLastUsedTime(managedToken); + SubjectSum subject = mockManagedSubjectWithClaims(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject); + + org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, new Object())); + verify(accountService).checkTokenStatus(managedToken); + verify(accountService).checkManagedTokenAccess("admin", List.of("admin")); + verify(accountService).touchTokenLastUsedTime(managedToken); + } + } + + @Test + void testManagedTokenRevokedRejected() throws Exception { + String revokedToken = "revoked-token"; + when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + revokedToken); + when(accountService.checkTokenStatus(revokedToken)).thenReturn("Token has been revoked"); + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(printWriter); + SubjectSum subject = mockManagedSubject(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject); + + org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, response, new Object())); + verify(response).setStatus(401); + } + } + + @Test + void testManagedTokenStatusCheckFailureRejectsRequest() throws Exception { + when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer managed-token"); + when(accountService.checkTokenStatus("managed-token")).thenThrow(new RuntimeException("DB down")); + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(printWriter); + SubjectSum subject = mockManagedSubject(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject); + + org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, response, new Object())); + verify(response).setStatus(503); + verify(accountService, never()).touchTokenLastUsedTime(any()); + } + } + + @Test + void testManagedTokenOutdatedRolesRejected() throws Exception { + String managedToken = "managed-token"; + when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + managedToken); + when(accountService.checkTokenStatus(managedToken)).thenReturn(null); + when(accountService.checkManagedTokenAccess("admin", List.of("admin"))) + .thenReturn("Token permissions are outdated"); + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(printWriter); + SubjectSum subject = mockManagedSubjectWithClaims(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject); + + org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, response, new Object())); + verify(response).setStatus(401); + verify(accountService, never()).touchTokenLastUsedTime(managedToken); + } + } + + @Test + void testEmptyBearerTokenPassesThrough() throws Exception { + when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer "); + SubjectSum subject = mockManagedSubject(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject); + + org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, new Object())); + } + } + + @Test + void testTouchLastUsedTimeFailureDoesNotRejectRequest() throws Exception { + String managedToken = "managed-token"; + when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + managedToken); + when(accountService.checkTokenStatus(managedToken)).thenReturn(null); + when(accountService.checkManagedTokenAccess("admin", List.of("admin"))).thenReturn(null); + // touchTokenLastUsedTime throws exception + org.mockito.Mockito.doThrow(new RuntimeException("DB error")) + .when(accountService).touchTokenLastUsedTime(managedToken); + SubjectSum subject = mockManagedSubjectWithClaims(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject); + + org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, new Object())); + } + } + + @Test + void testUnauthenticatedRequestSkipsValidation() throws Exception { + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(null); + + org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, new Object())); + verify(accountService, never()).checkTokenStatus(any()); + } + } + + @Test + void testManagedTokenAccountValidationFailureRejectsRequest() throws Exception { + String managedToken = "managed-token"; + when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + managedToken); + when(accountService.checkTokenStatus(managedToken)).thenReturn(null); + when(accountService.checkManagedTokenAccess("admin", List.of("admin"))) + .thenThrow(new RuntimeException("account store unavailable")); + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + when(response.getWriter()).thenReturn(printWriter); + SubjectSum subject = mockManagedSubjectWithClaims(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject); + + org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, response, new Object())); + verify(response).setStatus(503); + verify(accountService, never()).touchTokenLastUsedTime(managedToken); + } + } + + private SubjectSum mockSubject() { + SubjectSum subjectSum = org.mockito.Mockito.mock(SubjectSum.class); + when(subjectSum.getPrincipalMap()).thenReturn(principalMap); + return subjectSum; + } + + private SubjectSum mockManagedSubject() { + SubjectSum subjectSum = mockSubject(); + when(principalMap.getPrincipal("managed")).thenReturn(true); + return subjectSum; + } + + private SubjectSum mockManagedSubjectWithClaims() { + SubjectSum subjectSum = mockManagedSubject(); + when(subjectSum.getPrincipal()).thenReturn("admin"); + when(subjectSum.getRoles()).thenReturn(List.of("admin")); + return subjectSum; + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AccountControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AccountControllerTest.java new file mode 100644 index 0000000..488acf4 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AccountControllerTest.java @@ -0,0 +1,103 @@ +/* + * 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.manager.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.usthe.sureness.util.JsonWebTokenUtil; +import java.util.HashMap; +import java.util.Map; +import javax.naming.AuthenticationException; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.pojo.dto.LoginDto; +import org.apache.hertzbeat.manager.pojo.dto.TokenDto; +import org.apache.hertzbeat.manager.service.AccountService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link AccountController} + */ +@ExtendWith(MockitoExtension.class) +class AccountControllerTest { + + private MockMvc mockMvc; + + @InjectMocks + private AccountController accountController; + @Mock + private AccountService accountService; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(accountController).build(); + } + + @Test + void authGetToken() throws Exception { + JsonWebTokenUtil.setDefaultSecretKey("dKhaX0csgOCTlCxq20yhmUea6H6JIpSE2Rwp" + + "CyaFv0bwq2Eik0jdrKUtsA6bx3sDJeFV643R" + + "LnfKefTjsIfJLBa2YkhEqEGtcHDTNe4CU6+9" + + "dKhaX0csgOCTlCxq20yhmUea6H6JIpSE2Rwp"); + LoginDto loginDto = LoginDto.builder() + .identifier("admin") + .credential("hertzbeat") + .build(); + Map resp = new HashMap<>(2); + resp.put("token", "token"); + resp.put("refreshToken", "refreshToken"); + resp.put("role", "roles"); + Mockito.when(accountService.authGetToken(loginDto)).thenReturn(resp); + + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/auth/form") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(loginDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.token").exists()) + .andReturn(); + loginDto.setCredential("wrong_credential"); + Mockito.when(accountService.authGetToken(loginDto)).thenThrow(new AuthenticationException()); + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/auth/form") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(loginDto))) + .andExpect(jsonPath("$.code").value((int) CommonConstants.LOGIN_FAILED_CODE)) + .andReturn(); + } + + @Test + void refreshToken() throws Exception { + String refreshToken = "123456"; + Mockito.when(accountService.refreshToken(refreshToken)).thenThrow(new AuthenticationException()); + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/auth/refresh") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(new TokenDto(refreshToken)))) + .andExpect(jsonPath("$.code").value((int) CommonConstants.LOGIN_FAILED_CODE)) + .andReturn(); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AppControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AppControllerTest.java new file mode 100644 index 0000000..77c3616 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AppControllerTest.java @@ -0,0 +1,246 @@ +/* + * 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.manager.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.manager.pojo.dto.Hierarchy; +import org.apache.hertzbeat.manager.pojo.dto.MonitorDefineDto; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.apache.hertzbeat.manager.service.impl.AppServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link AppController} + */ +@ExtendWith(MockitoExtension.class) +class AppControllerTest { + + private MockMvc mockMvc; + + @Mock + private AppServiceImpl appService; + + @InjectMocks + private AppController appController; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(appController).build(); + } + + @Test + void queryAppParamDefines() throws Exception { + // Data to make + List paramDefines = new ArrayList<>(); + ParamDefineInfo paramDefine = new ParamDefineInfo(); + paramDefine.setId(1L); + paramDefine.setApp("tanCloud"); + paramDefine.setName(new HashMap<>()); + paramDefine.setField("port"); + paramDefine.setType("number"); + paramDefine.setDefaultValue("12"); + paramDefine.setPlaceholder("请输出密码"); + paramDefine.setCreator("tom"); + paramDefine.setModifier("tom"); + paramDefines.add(paramDefine); + + // The interface is called to return manufactured data + Mockito.when(appService.getAppParamDefines("api")) + .thenReturn(paramDefines); + + // Request interface + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/apps/{app}/params", "api")) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data[0].id").value(1)) + .andReturn(); + } + + @Test + void queryPushDefine() throws Exception { + // Data to make + Job mockJob = new Job(); + mockJob.setId(1L); + mockJob.setMonitorId(1L); + mockJob.setCategory("os"); + mockJob.setApp("mac"); + mockJob.setName(new HashMap<>()); + mockJob.setMetrics(new ArrayList<>()); + mockJob.setConfigmap(new ArrayList<>()); + + + // The interface is called to return manufactured data + Mockito.when(appService.getPushDefine(1L)).thenReturn(mockJob); + + // Request interface + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/apps/{monitorId}/pushdefine", 1L)) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.id").value(1)) + .andExpect(jsonPath("$.data.app").value("mac")) + .andReturn(); + } + + @Test + void queryAutoGenerateDynamicAppDefine() throws Exception { + // Data to make + Job mockJob = new Job(); + mockJob.setId(1L); + mockJob.setMonitorId(1L); + mockJob.setCategory("os"); + mockJob.setApp("huawei"); + mockJob.setName(new HashMap<>()); + mockJob.setMetrics(new ArrayList<>()); + mockJob.setConfigmap(new ArrayList<>()); + + // The interface is called to return manufactured data + Mockito.when(appService.getAutoGenerateDynamicDefine(1L)).thenReturn(mockJob); + + // Request interface + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/apps/{monitorId}/define/dynamic", 1L)) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.id").value(1)) + .andExpect(jsonPath("$.data.app").value("huawei")) + .andReturn(); + } + + @Test + void queryAppDefineYml() throws Exception { + // Data to make + String defineContent = "defineContent"; + + // The interface is called to return manufactured data + Mockito.when(appService.getMonitorDefineFileContent("api")) + .thenReturn(defineContent); + + // Request interface + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/apps/{app}/define/yml", "api")) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data").value(defineContent)) + .andReturn(); + } + + @Test + void deleteAppDefineYml_Success() throws Exception { + // Mock the service call + Mockito.doNothing().when(appService).deleteMonitorDefine("api"); + + // Perform the request and verify the response + this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/apps/{app}/define/yml", "api")) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + + // Verify that the service method was called + Mockito.verify(appService, Mockito.times(1)).deleteMonitorDefine("api"); + } + + @Test + void deleteAppDefineYml_Failure() throws Exception { + // Mock the service call to throw an exception + Mockito.doThrow(new RuntimeException("Delete failed")).when(appService).deleteMonitorDefine("api"); + + // Perform the request and verify the response + this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/apps/{app}/define/yml", "api")) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("Delete failed")) + .andReturn(); + + // Verify that the service method was called + Mockito.verify(appService, Mockito.times(1)).deleteMonitorDefine("api"); + } + + @Test + void newAppDefineYml() throws Exception { + // Data to make + MonitorDefineDto defineDto = new MonitorDefineDto(); + defineDto.setDefine("valid-define"); + String defineValue = "test-define"; + + String jsonString = "{\"define\":\"" + defineValue.replace("\"", "\\\"") + "\"}"; + + // The interface is called to return manufactured data + Mockito.doNothing().when(appService).applyMonitorDefineYml(Mockito.anyString(), Mockito.anyBoolean()); + + // Request interface + this.mockMvc.perform(post("/api/apps/define/yml") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonString)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void queryAppDefine() throws Exception { + // Data to make + Job define = new Job(); + define.setId(1L); + define.setMonitorId(1L); + define.setCategory("os"); + define.setApp("linux"); + define.setName(new HashMap<>()); + define.setMetrics(new ArrayList<>()); + define.setConfigmap(new ArrayList<>()); + + // The interface is called to return manufactured data + Mockito.when(appService.getAppDefine("api")) + .thenReturn(define); + + // Request interface + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/apps/{app}/define", "api")) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.id").value(1)) + .andReturn(); + } + + @Test + void queryAppsHierarchy() throws Exception { + // Data to make + Hierarchy hierarchy = new Hierarchy(); + hierarchy.setLabel("Linux系统"); + hierarchy.setValue("linux"); + hierarchy.setCategory("os"); + List list = new ArrayList<>(); + list.add(hierarchy); + + // The interface is called to return manufactured data + Mockito.when(appService.getAllAppHierarchy("zh-CN")) + .thenReturn(list); + + // Request interface + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/apps/hierarchy", "zh-CN")) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data[0].category").value("os")) + .andReturn(); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AuthTokenControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AuthTokenControllerTest.java new file mode 100644 index 0000000..5891086 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AuthTokenControllerTest.java @@ -0,0 +1,226 @@ +/* + * 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.manager.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +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 com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.SurenessContextHolder; +import java.time.LocalDateTime; +import java.util.List; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.manager.AuthToken; +import org.apache.hertzbeat.manager.service.AccountService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link AuthTokenController} + */ +@ExtendWith(MockitoExtension.class) +class AuthTokenControllerTest { + + private MockMvc mockMvc; + + @InjectMocks + private AuthTokenController authTokenController; + + @Mock + private AccountService accountService; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(authTokenController).build(); + } + + @Test + void testGenerateTokenSuccess() throws Exception { + SubjectSum subjectSum = mockAdminSubject(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + when(accountService.generateToken(eq("my-token"), eq(3600L))).thenReturn("generated-jwt-token"); + + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/token/generate") + .param("name", "my-token") + .param("expireSeconds", "3600")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.token").value("generated-jwt-token")); + } + } + + @Test + void testGenerateTokenWithoutOptionalParams() throws Exception { + SubjectSum subjectSum = mockAdminSubject(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + when(accountService.generateToken(any(), any())).thenReturn("generated-jwt-token"); + + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/token/generate")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.token").value("generated-jwt-token")); + } + } + + @Test + void testGenerateTokenNoLogin() throws Exception { + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(null); + + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/token/generate")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("No login user")); + } + } + + @Test + void testGenerateTokenNoPermission() throws Exception { + SubjectSum subjectSum = mockUserSubject(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/token/generate")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("No permission")); + } + } + + @Test + void testListTokensSuccess() throws Exception { + SubjectSum subjectSum = mockAdminSubject(); + List tokens = List.of( + AuthToken.builder().id(1L).name("Token1").tokenHash("hash1").tokenMask("mask1") + .status((byte) 0).gmtCreate(LocalDateTime.now()).build(), + AuthToken.builder().id(2L).name("Token2").tokenHash("hash2").tokenMask("mask2") + .status((byte) 0).gmtCreate(LocalDateTime.now()).build() + ); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + when(accountService.listTokens()).thenReturn(tokens); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/account/token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.length()").value(2)) + .andExpect(jsonPath("$.data[0].name").value("Token1")) + .andExpect(jsonPath("$.data[1].name").value("Token2")); + } + } + + @Test + void testListTokensNoLogin() throws Exception { + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(null); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/account/token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)); + } + } + + @Test + void testListTokensNoPermission() throws Exception { + SubjectSum subjectSum = mockUserSubject(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/account/token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("No permission")); + } + } + + @Test + void testDeleteTokenSuccess() throws Exception { + SubjectSum subjectSum = mockAdminSubject(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + doNothing().when(accountService).deleteToken(1L); + + this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/account/token/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + + verify(accountService).deleteToken(1L); + } + } + + @Test + void testDeleteTokenNoLogin() throws Exception { + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(null); + + this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/account/token/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)); + } + } + + @Test + void testDeleteTokenNoPermission() throws Exception { + SubjectSum subjectSum = mockUserSubject(); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/account/token/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("No permission")); + + verify(accountService, never()).deleteToken(1L); + } + } + + private SubjectSum mockAdminSubject() { + SubjectSum subjectSum = org.mockito.Mockito.mock(SubjectSum.class); + when(subjectSum.hasRole("admin")).thenReturn(true); + return subjectSum; + } + + private SubjectSum mockUserSubject() { + SubjectSum subjectSum = org.mockito.Mockito.mock(SubjectSum.class); + when(subjectSum.hasRole("admin")).thenReturn(false); + return subjectSum; + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/BulletinControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/BulletinControllerTest.java new file mode 100644 index 0000000..cc383f7 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/BulletinControllerTest.java @@ -0,0 +1,130 @@ +/* + * 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.manager.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +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.common.entity.manager.Bulletin; +import org.apache.hertzbeat.manager.pojo.dto.BulletinMetricsData; +import org.apache.hertzbeat.manager.service.BulletinService; +import org.apache.hertzbeat.warehouse.store.realtime.RealTimeDataReader; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link BulletinController} + */ +@ExtendWith(MockitoExtension.class) +class BulletinControllerTest { + private MockMvc mockMvc; + + @InjectMocks + private BulletinController bulletinController; + @Mock + private BulletinService bulletinService; + @Mock + private RealTimeDataReader realTimeDataReader; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(bulletinController).build(); + } + + @Test + void testAddNewBulletin() throws Exception { + Bulletin bulletinDto = new Bulletin(); + doAnswer(invocation -> { + throw new IllegalArgumentException("Invalid bulletin"); + }).when(bulletinService).validate(bulletinDto); + + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/bulletin") + .contentType("application/json") + .content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)); + + doAnswer(invocation -> { + return null; + }).when(bulletinService).validate(bulletinDto); + doAnswer(invocation -> { + return null; + }).when(bulletinService).addBulletin(bulletinDto); + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/bulletin") + .contentType("application/json") + .content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + + @Test + void testEditBulletin() throws Exception { + Bulletin bulletinDto = new Bulletin(); + doAnswer(invocation -> { + throw new IllegalArgumentException("Invalid bulletin"); + }).when(bulletinService).validate(bulletinDto); + + this.mockMvc.perform(MockMvcRequestBuilders.put("/api/bulletin") + .contentType("application/json") + .content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)); + + doAnswer(invocation -> { + return null; + }).when(bulletinService).validate(bulletinDto); + doAnswer(invocation -> { + return null; + }).when(bulletinService).editBulletin(bulletinDto); + this.mockMvc.perform(MockMvcRequestBuilders.put("/api/bulletin") + .contentType("application/json") + .content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + + @Test + void testGetAllMetricsData() throws Exception { + // server is not available + Mockito.when(realTimeDataReader.isServerAvailable()).thenReturn(false); + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/bulletin/metrics?id=33")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)); + + BulletinMetricsData data = new BulletinMetricsData(); + data.setName("sample"); + Mockito.when(realTimeDataReader.isServerAvailable()).thenReturn(true); + Mockito.when(bulletinService.buildBulletinMetricsData(any(Long.class))) + .thenReturn(data); + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/bulletin/metrics?id=33")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.name").value("sample")) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/CollectorControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/CollectorControllerTest.java new file mode 100644 index 0000000..9882eda --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/CollectorControllerTest.java @@ -0,0 +1,124 @@ +/* + * 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.manager.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.scheduler.netty.ManageServer; +import org.apache.hertzbeat.manager.service.impl.CollectorServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +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.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link CollectorController} + */ +@ExtendWith(MockitoExtension.class) +@Slf4j +public class CollectorControllerTest { + + private MockMvc mockMvc; + + @InjectMocks + private CollectorController collectorController; + + @Mock + private CollectorServiceImpl collectorService; + + @Mock + private ManageServer manageServer; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(collectorController).build(); + } + + @Test + public void getCollectors() throws Exception { + this.mockMvc.perform(MockMvcRequestBuilders.get( + "/api/collector?name={name}&pageIndex={pageIndex}&pageSize={pageSize}", + "tom", 0, 10)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + public void onlineCollector() throws Exception { + List collectors = new ArrayList<>(); + collectors.add("demo-collector"); + this.mockMvc.perform(MockMvcRequestBuilders.put( + "/api/collector/online") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(collectors))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + public void offlineCollector() throws Exception { + List collectors = new ArrayList<>(); + collectors.add("demo-collector"); + this.mockMvc.perform(MockMvcRequestBuilders.put( + "/api/collector/offline") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(collectors))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + + @Test + public void deleteCollector() throws Exception { + List collectors = new ArrayList<>(); + collectors.add("demo-collector"); + this.mockMvc.perform(MockMvcRequestBuilders.delete( + "/api/collector") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(collectors))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + public void generateCollectorDeployInfo() throws Exception { + this.mockMvc.perform(MockMvcRequestBuilders.post( + "/api/collector/generate/{collector}", + "demo-collector")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/GeneralConfigControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/GeneralConfigControllerTest.java new file mode 100644 index 0000000..77f912c --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/GeneralConfigControllerTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.manager.pojo.dto.TemplateConfig; +import org.apache.hertzbeat.manager.service.impl.ConfigServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +/** + * Test case for {@link GeneralConfigController} + */ + +@ExtendWith(MockitoExtension.class) +class GeneralConfigControllerTest { + + private MockMvc mockMvc; + + @Mock + private ConfigServiceImpl configService; + + @InjectMocks + private GeneralConfigController generalConfigController; + + @BeforeEach + public void setup() { + + mockMvc = standaloneSetup(generalConfigController).build(); + } + + @Test + public void testSaveOrUpdateConfig() throws Exception { + + doNothing().when(configService).saveConfig(anyString(), any()); + + mockMvc.perform(post("/api/config/email") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"key\":\"value\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Update config success")); + } + + @Test + public void testGetConfig() throws Exception { + + when(configService.getConfig(anyString())).thenReturn(any()); + + mockMvc.perform(get("/api/config/email") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + + @Test + public void testUpdateTemplateAppConfig() throws Exception { + + doNothing().when(configService).updateTemplateAppConfig(anyString(), any(TemplateConfig.AppTemplate.class)); + + mockMvc.perform(put("/api/config/template/appName") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"templateKey\":\"templateValue\",\"hide\":true}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/I18nControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/I18nControllerTest.java new file mode 100644 index 0000000..fbd8a29 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/I18nControllerTest.java @@ -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.manager.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import java.util.HashMap; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.manager.service.impl.AppServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link I18nController} + */ +@ExtendWith(MockitoExtension.class) +class I18nControllerTest { + + private MockMvc mockMvc; + + @Mock + private AppServiceImpl appService; + + @InjectMocks + private I18nController i18nController; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(i18nController).build(); + } + + @Test + void queryI18n() throws Exception { + + // The interface is called to return manufactured data + Mockito.when(appService.getI18nResources("zh-CN")) + .thenReturn(new HashMap<>()); + // Request interface + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/i18n/{lang}", "zh-CN")) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/LabelControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/LabelControllerTest.java new file mode 100644 index 0000000..a73d69e --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/LabelControllerTest.java @@ -0,0 +1,115 @@ +/* + * 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.manager.controller; + +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.common.entity.manager.Label; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.service.impl.LabelServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +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.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link LabelController} + */ +@ExtendWith(MockitoExtension.class) +class LabelControllerTest { + + private MockMvc mockMvc; + + @Mock + private LabelServiceImpl labelService; + + @InjectMocks + private LabelController labelController; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(labelController).build(); + } + + @Test + void addNewLabel() throws Exception { + Label label = new Label(); + label.setId(87584674384L); + label.setName("app"); + label.setTagValue("23"); + label.setType((byte) 1); + label.setCreator("tom"); + label.setModifier("tom"); + + + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/label") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(label))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Add success")) + .andReturn(); + } + + @Test + void modifyLabel() throws Exception { + Label tag = new Label(); + tag.setId(87584674384L); + tag.setName("app"); + tag.setTagValue("23"); + tag.setType((byte) 1); + tag.setCreator("tom"); + tag.setModifier("tom"); + + this.mockMvc.perform(MockMvcRequestBuilders.put("/api/label") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(tag))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Modify success")) + .andReturn(); + } + + @Test + void getLabels() throws Exception { + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/label?type={type}&search={search}", + (byte) 1, "status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void deleteLabels() throws Exception { + this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/label") + .contentType(MediaType.MULTIPART_FORM_DATA) + .param("ids", "6565463543")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Delete success")) + .andReturn(); + } + +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MetricsControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MetricsControllerTest.java new file mode 100644 index 0000000..0ddba50 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MetricsControllerTest.java @@ -0,0 +1,75 @@ +/* + * 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.manager.controller; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; +import java.util.HashMap; +import java.util.Map; +import org.apache.hertzbeat.common.queue.impl.InMemoryCommonDataQueue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +/** + * Test case for {@link MetricsController} + */ + +@ExtendWith(MockitoExtension.class) +class MetricsControllerTest { + + private MockMvc mockMvc; + + @Mock + private InMemoryCommonDataQueue commonDataQueue; + + @InjectMocks + private MetricsController metricsController; + + @BeforeEach + public void setup() { + + mockMvc = standaloneSetup(metricsController).build(); + } + + @Test + public void testGetMetricsInfo() throws Exception { + + Map queueInfo = new HashMap<>(); + queueInfo.put("metric1", 100); + queueInfo.put("metric2", 200); + + when(commonDataQueue.getQueueSizeMetricsInfo()).thenReturn(queueInfo); + + mockMvc.perform(get("/api/metrics") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.metric1").value(100)) + .andExpect(jsonPath("$.data.metric2").value(200)); + } + +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MetricsFavoriteControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MetricsFavoriteControllerTest.java new file mode 100644 index 0000000..f926a47 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MetricsFavoriteControllerTest.java @@ -0,0 +1,255 @@ +/* + * 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.manager.controller; + +import com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.SurenessContextHolder; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.manager.service.MetricsFavoriteService; +import org.apache.hertzbeat.manager.support.GlobalExceptionHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Set; + +import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_CONFLICT_CODE; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Test case for {@link MetricsFavoriteController} + */ +@ExtendWith(MockitoExtension.class) +class MetricsFavoriteControllerTest { + + private MockMvc mockMvc; + + @Mock + private MetricsFavoriteService metricsFavoriteService; + + @InjectMocks + private MetricsFavoriteController metricsFavoriteController; + + private final Long testMonitorId = 1L; + private final String testMetricsName = "cpu"; + private final String testUserId = "testUser"; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(metricsFavoriteController) + .setControllerAdvice(new GlobalExceptionHandler()) + .setMessageConverters(new JacksonJsonHttpMessageConverter()) + .build(); + } + + @Test + void testAddMetricsFavoriteSuccess() throws Exception { + SubjectSum subjectSum = mock(SubjectSum.class); + when(subjectSum.getPrincipal()).thenReturn(testUserId); + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + doNothing().when(metricsFavoriteService).addMetricsFavorite(testUserId, testMonitorId, testMetricsName); + + mockMvc.perform(post("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Metrics added to favorites successfully")); + + verify(metricsFavoriteService).addMetricsFavorite(testUserId, testMonitorId, testMetricsName); + } + } + + @Test + void testAddMetricsFavoriteAlreadyExists() throws Exception { + SubjectSum subjectSum = mock(SubjectSum.class); + when(subjectSum.getPrincipal()).thenReturn(testUserId); + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + doThrow(new RuntimeException("Metrics favorite already exists: " + testMetricsName)) + .when(metricsFavoriteService).addMetricsFavorite(testUserId, testMonitorId, testMetricsName); + + mockMvc.perform(post("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.code").value((int) MONITOR_CONFLICT_CODE)) + .andExpect(jsonPath("$.msg").value("Metrics favorite already exists: " + testMetricsName)); + + verify(metricsFavoriteService).addMetricsFavorite(testUserId, testMonitorId, testMetricsName); + } + } + + @Test + void testAddMetricsFavoriteMissingParameters() throws Exception { + mockMvc.perform(post("/api/metrics/favorite") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is5xxServerError()); + + verify(metricsFavoriteService, never()).addMetricsFavorite(anyString(), anyLong(), anyString()); + } + + @Test + void testRemoveMetricsFavoriteSuccess() throws Exception { + SubjectSum subjectSum = mock(SubjectSum.class); + when(subjectSum.getPrincipal()).thenReturn(testUserId); + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + // Given + doNothing().when(metricsFavoriteService).removeMetricsFavorite(testUserId, testMonitorId, testMetricsName); + + // When & Then + mockMvc.perform(delete("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Metrics removed from favorites successfully")); + + verify(metricsFavoriteService).removeMetricsFavorite(testUserId, testMonitorId, testMetricsName); + } + } + + @Test + void testRemoveMetricsFavoriteException() throws Exception { + SubjectSum subjectSum = mock(SubjectSum.class); + when(subjectSum.getPrincipal()).thenReturn(testUserId); + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + doThrow(new RuntimeException("Database error")) + .when(metricsFavoriteService).removeMetricsFavorite(testUserId, testMonitorId, testMetricsName); + + mockMvc.perform(delete("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.code").value((int) MONITOR_CONFLICT_CODE)) + .andExpect(jsonPath("$.msg").value("Database error")); + + verify(metricsFavoriteService).removeMetricsFavorite(testUserId, testMonitorId, testMetricsName); + } + } + + @Test + void testGetUserFavoritedMetricsSuccess() throws Exception { + SubjectSum subjectSum = mock(SubjectSum.class); + when(subjectSum.getPrincipal()).thenReturn(testUserId); + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + Set favoriteMetrics = Set.of("cpu", "memory", "disk"); + when(metricsFavoriteService.getUserFavoritedMetrics(testUserId, testMonitorId)) + .thenReturn(favoriteMetrics); + + mockMvc.perform(get("/api/metrics/favorite/{monitorId}", testMonitorId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(3)); + + verify(metricsFavoriteService).getUserFavoritedMetrics(testUserId, testMonitorId); + } + } + + @Test + void testGetUserFavoritedMetricsEmptyResult() throws Exception { + SubjectSum subjectSum = mock(SubjectSum.class); + when(subjectSum.getPrincipal()).thenReturn(testUserId); + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + Set favoriteMetrics = Set.of(); + when(metricsFavoriteService.getUserFavoritedMetrics(testUserId, testMonitorId)) + .thenReturn(favoriteMetrics); + + mockMvc.perform(get("/api/metrics/favorite/{monitorId}", testMonitorId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(0)); + + verify(metricsFavoriteService).getUserFavoritedMetrics(testUserId, testMonitorId); + } + } + + @Test + void testGetUserFavoritedMetricsException() throws Exception { + SubjectSum subjectSum = mock(SubjectSum.class); + when(subjectSum.getPrincipal()).thenReturn(testUserId); + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + when(metricsFavoriteService.getUserFavoritedMetrics(testUserId, testMonitorId)) + .thenThrow(new RuntimeException("Service error")); + + mockMvc.perform(get("/api/metrics/favorite/{monitorId}", testMonitorId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.code").value((int) MONITOR_CONFLICT_CODE)) + .andExpect(jsonPath("$.msg").value("Service error")); + + verify(metricsFavoriteService).getUserFavoritedMetrics(testUserId, testMonitorId); + } + } + + @Test + void testGetUserFavoritedMetricsMissingMonitorId() throws Exception { + mockMvc.perform(get("/api/metrics/favorite") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is5xxServerError()); + + verify(metricsFavoriteService, never()).getUserFavoritedMetrics(anyString(), anyLong()); + } + + @Test + void testUnauthenticatedAccess() throws Exception { + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(null); + mockMvc.perform(post("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.LOGIN_FAILED_CODE)) + .andExpect(jsonPath("$.msg").value("User not authenticated")); + + verify(metricsFavoriteService, never()).addMetricsFavorite(anyString(), anyLong(), anyString()); + } + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java new file mode 100644 index 0000000..b30c392 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java @@ -0,0 +1,173 @@ +/* + * 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.manager.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.ArrayList; +import java.util.List; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.pojo.dto.MonitorDto; +import org.apache.hertzbeat.manager.service.impl.MonitorServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link MonitorController} + */ +@ExtendWith(MockitoExtension.class) +class MonitorControllerTest { + + private MockMvc mockMvc; + + @Mock + private MonitorServiceImpl monitorService; + + @InjectMocks + private MonitorController monitorController; + + public MonitorDto dataTest() { + Monitor monitor = new Monitor(); + monitor.setApp("website"); + monitor.setId(87584674384L); + monitor.setJobId(43243543543L); + monitor.setName("Api-TanCloud.cn"); + monitor.setName("TanCloud"); + monitor.setInstance("192.167.25.11:8989"); + monitor.setIntervals(600); + monitor.setDescription("对SAAS网站TanCloud的可用性监控"); + monitor.setCreator("tom"); + monitor.setModifier("tom"); + + List params = new ArrayList<>(); + Param param = new Param(); + param.setField("host"); + param.setParamValue("124.222.98.77"); + params.add(param); + + MonitorDto monitorDto = new MonitorDto(); + monitorDto.setMonitor(monitor); + monitorDto.setParams(params); + return monitorDto; + } + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(monitorController).build(); + } + + @Test + void addNewMonitor() throws Exception { + + MonitorDto monitorDto = dataTest(); + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/monitor") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(monitorDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + + } + + @Test + void modifyMonitor() throws Exception { + MonitorDto monitorDto = dataTest(); + + this.mockMvc.perform(MockMvcRequestBuilders.put("/api/monitor") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(monitorDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void getMonitor() throws Exception { + Monitor monitor = new Monitor(); + monitor.setId(87584674384L); + monitor.setJobId(43243543543L); + monitor.setName("Api-TanCloud.cn"); + monitor.setName("TanCloud"); + monitor.setInstance("192.167.25.11:8989"); + monitor.setIntervals(600); + monitor.setDescription("对SAAS网站TanCloud的可用性监控"); + monitor.setCreator("tom"); + monitor.setModifier("tom"); + + MonitorDto monitorDto = new MonitorDto(); + monitorDto.setMonitor(monitor); + + + Mockito.when(monitorService.getMonitorDto(6565463543L)) + .thenReturn(monitorDto); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/monitor/{id}", 6565463543L)) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(status().isOk()) + .andReturn(); + } + + @Test + void deleteMonitor() throws Exception { + + Monitor monitor = new Monitor(); + monitor.setId(87584674384L); + monitor.setJobId(43243543543L); + monitor.setName("Api-TanCloud.cn"); + monitor.setName("TanCloud"); + monitor.setInstance("192.167.25.11:8989"); + monitor.setIntervals(600); + monitor.setDescription("对SAAS网站TanCloud的可用性监控"); + monitor.setCreator("tom"); + monitor.setModifier("tom"); + + Mockito.when(monitorService.getMonitor(6565463543L)) + .thenReturn(monitor); + + this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/monitor/{id}", 6565463543L)) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Delete success")) + .andExpect(status().isOk()) + .andReturn(); + } + + @Test + void detectMonitor() throws Exception { + MonitorDto monitorDto = dataTest(); + + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/monitor/detect") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(monitorDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Detect success.")) + .andReturn(); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorsControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorsControllerTest.java new file mode 100644 index 0000000..f5026a2 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorsControllerTest.java @@ -0,0 +1,185 @@ +/* + * 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.manager.controller; + +import static org.mockito.Mockito.doNothing; +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 java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.service.impl.MonitorServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.springframework.data.domain.PageImpl; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.web.config.EnableSpringDataWebSupport; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.data.web.config.SpringDataJackson3Configuration; +import org.springframework.data.web.config.SpringDataWebSettings; +import tools.jackson.databind.json.JsonMapper; + +/** + * Test case for {@link MonitorsController} + */ +@ExtendWith(MockitoExtension.class) +class MonitorsControllerTest { + + private MockMvc mockMvc; + + @Mock + private MonitorServiceImpl monitorService; + + @InjectMocks + private MonitorsController monitorsController; + + @BeforeEach + void setUp() { + JacksonJsonHttpMessageConverter messageConverter = new JacksonJsonHttpMessageConverter( + JsonMapper.builder() + .addModule(new SpringDataJackson3Configuration.PageModule( + new SpringDataWebSettings(EnableSpringDataWebSupport.PageSerializationMode.DIRECT))) + .build()); + this.mockMvc = MockMvcBuilders.standaloneSetup(monitorsController) + .setMessageConverters(messageConverter) + .build(); + } + + @Test + void getMonitors() throws Exception { + Monitor monitor = Monitor.builder().id(6565463543L).name("website-prod").app("website").build(); + when(monitorService.getMonitors(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.anyInt(), Mockito.anyInt(), Mockito.any())) + .thenReturn(new PageImpl<>(List.of(monitor))); + + this.mockMvc.perform(MockMvcRequestBuilders.get( + "/api/monitors?app={app}&ids={ids}&host={host}&id={id}", + "website", + 6565463543L, + "127.0.0.1", + "id" + )) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.content[0].name").value("website-prod")) + .andReturn(); + } + + @Test + void getAppMonitors() throws Exception { + when(monitorService.getAppMonitors("linux")) + .thenReturn(List.of(Monitor.builder().id(1L).name("linux-prod").app("linux").build())); + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/monitors/{app}", "linux")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data[0].name").value("linux-prod")) + .andReturn(); + } + + @Test + void deleteMonitors() throws Exception { + List ids = new ArrayList<>(); + ids.add(6565463543L); + + this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/monitors") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(ids))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void cancelManageMonitors() throws Exception { + List ids = new ArrayList<>(); + ids.add(6565463543L); + + this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/monitors/manage") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(ids))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void enableManageMonitors() throws Exception { + List ids = new ArrayList<>(); + ids.add(6565463543L); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/monitors/manage") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(ids))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void export() throws Exception { + List ids = Arrays.asList(6565463543L, 6565463544L); + String type = "JSON"; + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/monitors/export") + .param("ids", ids.stream().map(String::valueOf).collect(Collectors.joining(","))) + .param("type", type)) + .andExpect(status().isOk()) + .andReturn(); + } + + @Test + void export2() throws Exception { + // Mock the behavior of monitorService.importConfig + doNothing().when(monitorService).importConfig(Mockito.any()); + + // Perform the request and verify the response + this.mockMvc.perform(MockMvcRequestBuilders.post("/api/monitors/import") + .contentType(MediaType.MULTIPART_FORM_DATA) + .param("file", "testFileContent")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("0")) + .andExpect(jsonPath("$.msg").value("Import success")); + } + + @Test + void exportAll() throws Exception { + String type = "JSON"; + + // Mock the behavior of monitorService.exportAll + doNothing().when(monitorService).exportAll(Mockito.anyString(), Mockito.any()); + + // Perform the request and verify the response + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/monitors/export/all") + .param("type", type)) + .andExpect(status().isOk()) + .andReturn(); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/PluginControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/PluginControllerTest.java new file mode 100644 index 0000000..a1991e2 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/PluginControllerTest.java @@ -0,0 +1,118 @@ +/* + * 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.manager.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.manager.PluginMetadata; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.service.PluginService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * test case for plugin controller + */ +@ExtendWith(MockitoExtension.class) +class PluginControllerTest { + + private MockMvc mockMvc; + + @InjectMocks + private PluginController pluginController; + + @Mock + private PluginService pluginService; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(pluginController).build(); + } + + @Test + void uploadNewPlugin() throws Exception { + MockMultipartFile jarFile = new MockMultipartFile( + "jarFile", + "plugin-test.jar", + "application/java-archive", + "This is the file content".getBytes(StandardCharsets.UTF_8) + ); + + this.mockMvc.perform(MockMvcRequestBuilders.multipart("/api/plugin") + .file(jarFile) + .contentType(MediaType.MULTIPART_FORM_DATA) + .param("name", "test-plugin") + .param("enableStatus", "true")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Add success")) + .andReturn(); + } + + @Test + void getPlugins() throws Exception { + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/plugin?&search={search}", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void deleteTags() throws Exception { + List ids = new ArrayList<>(); + ids.add(6565463543L); + + this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/plugin") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(ids))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Delete success")) + .andReturn(); + } + + @Test + void updatePluginStatus() throws Exception { + PluginMetadata metadata = new PluginMetadata(); + metadata.setId(6565463543L); + metadata.setEnableStatus(true); + + this.mockMvc.perform(MockMvcRequestBuilders.put("/api/plugin") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(metadata))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Update success")) + .andReturn(); + } + +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/StatusPageControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/StatusPageControllerTest.java new file mode 100644 index 0000000..06cfcff --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/StatusPageControllerTest.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.controller; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; + +import java.util.Collections; +import java.util.List; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageComponentInfo; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageIncidentInfo; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageOrgInfo; +import org.apache.hertzbeat.manager.service.StatusPageService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +/** + * test case for {@link StatusPageController} + */ + +@ExtendWith(MockitoExtension.class) +class StatusPageControllerTest { + + private MockMvc mockMvc; + + @Mock + private StatusPageService statusPageService; + + @InjectMocks + private StatusPageController statusPageController; + + @BeforeEach + public void setup() { + + mockMvc = standaloneSetup(statusPageController).build(); + } + + @Test + public void testQueryStatusPageOrg() throws Exception { + + StatusPageOrgInfo statusPageOrg = new StatusPageOrgInfo(); + when(statusPageService.queryStatusPageOrg()).thenReturn(statusPageOrg); + + mockMvc.perform(get("/api/status/page/org") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + + @Test + public void testQueryStatusPageOrg_NotFound() throws Exception { + + when(statusPageService.queryStatusPageOrg()).thenReturn(null); + + mockMvc.perform(get("/api/status/page/org") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("Status Page Organization Not Found")); + } + + @Test + public void testSaveStatusPageOrg() throws Exception { + + StatusPageOrgInfo statusPageOrg = new StatusPageOrgInfo(); + statusPageOrg.setName("Test name"); + statusPageOrg.setHome("Test home"); + statusPageOrg.setDescription("Test description"); + statusPageOrg.setLogo("Test logo"); + when(statusPageService.saveStatusPageOrg(statusPageOrg)).thenReturn(statusPageOrg); + + mockMvc.perform(post("/api/status/page/org") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(statusPageOrg)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + + @Test + public void testQueryStatusPageComponent() throws Exception { + + List components = Collections.singletonList(new StatusPageComponentInfo()); + components.get(0).setId(1L); + components.get(0).setName("Gateway"); + when(statusPageService.queryStatusPageComponents()).thenReturn(components); + + mockMvc.perform(get("/api/status/page/component") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data[0].name").value("Gateway")); + } + + @Test + public void testNewStatusPageComponent() throws Exception { + + mockMvc.perform(post("/api/status/page/component") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"New Component\",\"method\":0,\"configState\":0,\"state\":0}") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Add success")); + } + + @Test + public void testUpdateStatusPageComponent() throws Exception { + + mockMvc.perform(put("/api/status/page/component") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"Updated Component\",\"method\":0,\"configState\":0,\"state\":0}") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Update success")); + } + + @Test + public void testDeleteStatusPageComponent() throws Exception { + + mockMvc.perform(delete("/api/status/page/component/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Delete success")); + } + + @Test + public void testQueryStatusPageComponentById() throws Exception { + + StatusPageComponentInfo component = new StatusPageComponentInfo(); + component.setId(1L); + component.setName("API Gateway"); + when(statusPageService.queryStatusPageComponent(1L)).thenReturn(component); + + mockMvc.perform(get("/api/status/page/component/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.name").value("API Gateway")); + } + + @Test + public void testNewStatusPageIncident() throws Exception { + + StatusPageIncidentInfo statusPageIncident = new StatusPageIncidentInfo(); + statusPageIncident.setName("New Incident"); + + mockMvc.perform(post("/api/status/page/incident") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(statusPageIncident)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Add success")); + } + + @Test + public void testUpdateStatusPageIncident() throws Exception { + + StatusPageIncidentInfo statusPageIncident = new StatusPageIncidentInfo(); + statusPageIncident.setName("Update Incident"); + + mockMvc.perform(put("/api/status/page/incident") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(statusPageIncident)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + + @Test + public void testDeleteStatusPageIncident() throws Exception { + + mockMvc.perform(delete("/api/status/page/incident/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.msg").value("Delete success")); + } + + @Test + public void testQueryStatusPageIncidentById() throws Exception { + + StatusPageIncidentInfo incident = new StatusPageIncidentInfo(); + when(statusPageService.queryStatusPageIncident(1L)).thenReturn(incident); + + mockMvc.perform(get("/api/status/page/incident/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + + @Test + public void testQueryStatusPageIncident() throws Exception { + mockMvc.perform(get("/api/status/page/incident?pageIndex=0&pageSize=10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/StatusPagePublicControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/StatusPagePublicControllerTest.java new file mode 100644 index 0000000..2c2d768 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/StatusPagePublicControllerTest.java @@ -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.manager.controller; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; + +import java.util.Collections; +import java.util.List; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.manager.pojo.dto.ComponentStatus; +import org.apache.hertzbeat.manager.pojo.dto.StatusPageOrgInfo; +import org.apache.hertzbeat.manager.service.StatusPageService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +/** + * test case for {@link StatusPagePublicController} + */ + +@ExtendWith(MockitoExtension.class) +class StatusPagePublicControllerTest { + + private MockMvc mockMvc; + + @Mock + private StatusPageService statusPageService; + + @InjectMocks + private StatusPagePublicController statusPagePublicController; + + @BeforeEach + public void setup() { + + mockMvc = standaloneSetup(statusPagePublicController).build(); + } + + @Test + public void testQueryStatusPageOrg() throws Exception { + + StatusPageOrgInfo statusPageOrg = new StatusPageOrgInfo(); + when(statusPageService.queryStatusPageOrg()).thenReturn(statusPageOrg); + + mockMvc.perform(get("/api/status/page/public/org") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + + @Test + public void testQueryStatusPageOrgNotFound() throws Exception { + + when(statusPageService.queryStatusPageOrg()).thenReturn(null); + + mockMvc.perform(get("/api/status/page/public/org") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andExpect(jsonPath("$.msg").value("Status Page Organization Not Found")); + } + + @Test + public void testQueryStatusPageComponent() throws Exception { + + List componentStatusList = Collections.singletonList(new ComponentStatus()); + when(statusPageService.queryComponentsStatus()).thenReturn(componentStatusList); + + mockMvc.perform(get("/api/status/page/public/component") + .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); + + mockMvc.perform(get("/api/status/page/public/component/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + + @Test + public void testQueryStatusPageIncident() throws Exception { + + mockMvc.perform(get("/api/status/page/public/incident")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)); + } + +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/SummaryControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/SummaryControllerTest.java new file mode 100644 index 0000000..4e0753c --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/SummaryControllerTest.java @@ -0,0 +1,77 @@ +/* + * 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.manager.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.ArrayList; +import java.util.List; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.manager.pojo.dto.AppCount; +import org.apache.hertzbeat.manager.service.impl.MonitorServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link SummaryController} + */ +@ExtendWith(MockitoExtension.class) +class SummaryControllerTest { + + private MockMvc mockMvc; + + @Mock + private MonitorServiceImpl monitorService; + + @InjectMocks + private SummaryController summaryController; + + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(summaryController).build(); + } + + @Test + void appMonitors() throws Exception { + List appsCounts = new ArrayList<>(); + + AppCount appCount = new AppCount(); + appCount.setApp("os"); + appCount.setCategory("mysql"); + appCount.setStatus((byte) 1); + appsCounts.add(appCount); + + Mockito.when(monitorService.getAllAppMonitorsCount()) + .thenReturn(appsCounts); + + this.mockMvc.perform(MockMvcRequestBuilders.get("/api/summary")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.apps[0].app").value("os")) + .andReturn(); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/scheduler/CollectorJobSchedulerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/scheduler/CollectorJobSchedulerTest.java new file mode 100644 index 0000000..10cc176 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/scheduler/CollectorJobSchedulerTest.java @@ -0,0 +1,194 @@ +/* + * 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.manager.scheduler; + +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.CollectorInfo; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.manager.CollectorMonitorBind; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.message.ClusterMsg; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.dao.CollectorDao; +import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao; +import org.apache.hertzbeat.manager.dao.MonitorDao; +import org.apache.hertzbeat.manager.dao.ParamDao; +import org.apache.hertzbeat.manager.scheduler.netty.ManageServer; +import org.apache.hertzbeat.manager.service.AppService; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test case for {@link CollectorJobScheduler} + */ +@ExtendWith(MockitoExtension.class) +public class CollectorJobSchedulerTest { + @InjectMocks + private CollectorJobScheduler collectorJobScheduler; + @Mock + private ConsistentHash consistentHash; + @Mock + private CollectorDao collectorDao; + @Mock + private CollectorMonitorBindDao collectorMonitorBindDao; + @Mock + private MonitorDao monitorDao; + @Mock + private ParamDao paramDao; + @Mock + private AppService appService; + + + @BeforeEach + void setUp() { + + } + + @Test + public void testCollectSyncJobData() { + assertDoesNotThrow(() -> { + Job job = new Job(); + when(consistentHash.preDispatchJob(any(String.class))).thenReturn(null); + List list = collectorJobScheduler.collectSyncJobData(job); + assertEquals(1, list.size()); + }); + } + + @Test + public void testCollectSyncJobResource() { + assertDoesNotThrow(() -> { + collectorJobScheduler.collectSyncJobResponse(null); + collectorJobScheduler.collectSyncJobResponse(new ArrayList<>()); + + List metricsDataList = new ArrayList(); + metricsDataList.add(CollectRep.MetricsData.newBuilder().build()); + collectorJobScheduler.collectSyncJobResponse(metricsDataList); + }); + } + + @Test + public void testCollectorGoOnlineJobMetadataNotEmpty() { + String identity = "collector-1"; + CollectorInfo collectorInfo = CollectorInfo.builder().ip("127.0.0.1").mode("public").version("1.0.0").build(); + org.apache.hertzbeat.common.entity.manager.Collector collector = org.apache.hertzbeat.common.entity.manager.Collector.builder() + .name(identity).ip("127.0.0.1").mode("public").version("1.0.0").status(CommonConstants.COLLECTOR_STATUS_OFFLINE).build(); + when(collectorDao.findCollectorByName(identity)).thenReturn(Optional.of(collector)); + + // mock bind + CollectorMonitorBind bind = CollectorMonitorBind.builder().collector(identity).monitorId(1L).build(); + when(collectorMonitorBindDao.findCollectorMonitorBindsByCollector(identity)).thenReturn(List.of(bind)); + + // mock monitor + Monitor monitor = Monitor.builder().id(1L).name("test-monitor").instance("127.0.0.1:8080").app("test-app").intervals(60).status((byte) 1).build(); + when(monitorDao.findMonitorsByIdIn(any())).thenReturn(List.of(monitor)); + + // mock Params + when(paramDao.findParamsByMonitorId(eq(monitor.getId()))).thenReturn(List.of()); + + // mock appDefine + Job appDefine = new Job(); + appDefine.setParams(Collections.emptyList()); + when(appService.getAppDefine(anyString())).thenReturn(appDefine); + + ConsistentHash.Node node = new ConsistentHash.Node(identity, collector.getMode(), + collector.getIp(), System.currentTimeMillis(), null); + when(consistentHash.getNode("collector-1")).thenReturn(node); + + ManageServer manageServer = mock(ManageServer.class); + collectorJobScheduler.setManageServer(manageServer); + // run + collectorJobScheduler.collectorGoOnline(identity, collectorInfo); + + // Capture the parameters of sendMsg + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(ClusterMsg.Message.class); + verify(manageServer, atLeastOnce()).sendMsg(eq("collector-1"), msgCaptor.capture()); + ClusterMsg.Message message = msgCaptor.getValue(); + + Job job = JsonUtil.fromJson(message.getMsg().toStringUtf8(), Job.class); + assertNotNull(job); + assertNotNull(job.getMetadata()); + assertEquals("test-monitor", job.getMetadata().get(CommonConstants.LABEL_INSTANCE_NAME)); + assertEquals("127.0.0.1:8080", job.getMetadata().get(CommonConstants.LABEL_INSTANCE)); + } + + /** + * Regression test for issue #4157: when a collector goes online the dispatched job must carry the + * monitor's cron schedule type and cron expression, otherwise the job falls back to interval + * scheduling and the cron expression is ignored after restart / collector reconnect. + */ + @Test + public void testCollectorGoOnlinePropagatesCronSchedule() { + String identity = "collector-1"; + CollectorInfo collectorInfo = CollectorInfo.builder().ip("127.0.0.1").mode("public").version("1.0.0").build(); + org.apache.hertzbeat.common.entity.manager.Collector collector = org.apache.hertzbeat.common.entity.manager.Collector.builder() + .name(identity).ip("127.0.0.1").mode("public").version("1.0.0").status(CommonConstants.COLLECTOR_STATUS_OFFLINE).build(); + when(collectorDao.findCollectorByName(identity)).thenReturn(Optional.of(collector)); + + CollectorMonitorBind bind = CollectorMonitorBind.builder().collector(identity).monitorId(1L).build(); + when(collectorMonitorBindDao.findCollectorMonitorBindsByCollector(identity)).thenReturn(List.of(bind)); + + Monitor monitor = Monitor.builder().id(1L).name("test-monitor").instance("127.0.0.1:8080").app("test-app") + .intervals(60).status((byte) 1).scheduleType("cron").cronExpression("0 55 7 * * ?").build(); + when(monitorDao.findMonitorsByIdIn(any())).thenReturn(List.of(monitor)); + + when(paramDao.findParamsByMonitorId(eq(monitor.getId()))).thenReturn(List.of()); + + Job appDefine = new Job(); + appDefine.setParams(Collections.emptyList()); + when(appService.getAppDefine(anyString())).thenReturn(appDefine); + + ConsistentHash.Node node = new ConsistentHash.Node(identity, collector.getMode(), + collector.getIp(), System.currentTimeMillis(), null); + when(consistentHash.getNode("collector-1")).thenReturn(node); + + ManageServer manageServer = mock(ManageServer.class); + collectorJobScheduler.setManageServer(manageServer); + + collectorJobScheduler.collectorGoOnline(identity, collectorInfo); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(ClusterMsg.Message.class); + verify(manageServer, atLeastOnce()).sendMsg(eq("collector-1"), msgCaptor.capture()); + Job job = JsonUtil.fromJson(msgCaptor.getValue().getMsg().toStringUtf8(), Job.class); + assertNotNull(job); + assertEquals("cron", job.getScheduleType()); + assertEquals("0 55 7 * * ?", job.getCronExpression()); + } + +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/scheduler/ConsistentHashTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/scheduler/ConsistentHashTest.java new file mode 100644 index 0000000..3555f84 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/scheduler/ConsistentHashTest.java @@ -0,0 +1,100 @@ +/* + * 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.manager.scheduler; + +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.assertTrue; +import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link ConsistentHash} + */ +public class ConsistentHashTest { + + private ConsistentHash consistentHash; + + @BeforeEach + void setUp() { + consistentHash = new ConsistentHash(); + } + + @Test + void testAddNode() { + String job1 = "job1"; + long jobId1 = SnowFlakeIdGenerator.generateId(); + String job2 = "job2"; + long jobId2 = SnowFlakeIdGenerator.generateId(); + String job3 = "job3"; + long jobId3 = SnowFlakeIdGenerator.generateId(); + ConsistentHash.Node node1 = new ConsistentHash.Node("node1", "public", "192.168.0.1", System.currentTimeMillis(), (byte) 10); + ConsistentHash.Node node2 = new ConsistentHash.Node("node2", "public", "192.168.0.2", System.currentTimeMillis(), (byte) 10); + consistentHash.addNode(node1); + consistentHash.dispatchJob(job1, jobId1); + consistentHash.dispatchJob(job2, jobId2); + consistentHash.dispatchJob(job3, jobId3); + consistentHash.addNode(node2); + assertTrue(node2.getAssignJobs().getAddingJobs().containsAll(node1.getAssignJobs().getRemovingJobs())); + assertTrue(node2.getAssignJobs().getAddingJobs().containsAll(node2.getAssignJobs().getRemovingJobs())); + assertSame(consistentHash.getNode("node1"), node1); + assertSame(consistentHash.getNode("node2"), node2); + } + + @Test + void testDispatchJob() { + String job1 = "job1"; + long jobId1 = SnowFlakeIdGenerator.generateId(); + ConsistentHash.Node res1 = consistentHash.dispatchJob(job1, jobId1); + assertNull(res1); + ConsistentHash.Node node1 = new ConsistentHash.Node("node1", "public", "192.168.0.1", System.currentTimeMillis(), (byte) 10); + consistentHash.addNode(node1); + String job2 = "job2"; + long jobId2 = SnowFlakeIdGenerator.generateId(); + ConsistentHash.Node res2 = consistentHash.dispatchJob(job2, jobId2); + assertSame(res2, node1); + assertTrue(consistentHash.getDispatchJobCache().isEmpty()); + } + + @Test + void testRemoveNode() { + String job1 = "job1"; + long jobId1 = SnowFlakeIdGenerator.generateId(); + String job2 = "job2"; + long jobId2 = SnowFlakeIdGenerator.generateId(); + String job3 = "job3"; + long jobId3 = SnowFlakeIdGenerator.generateId(); + ConsistentHash.Node node1 = new ConsistentHash.Node("node1", "public", "192.168.0.1", System.currentTimeMillis(), (byte) 10); + ConsistentHash.Node node2 = new ConsistentHash.Node("node2", "public", "192.168.0.2", System.currentTimeMillis(), (byte) 10); + consistentHash.addNode(node1); + consistentHash.addNode(node2); + consistentHash.dispatchJob(job1, jobId1); + consistentHash.dispatchJob(job2, jobId2); + consistentHash.dispatchJob(job3, jobId3); + consistentHash.removeNode(node2.getIdentity()); + assertTrue(node1.getAssignJobs().getAddingJobs().containsAll(node2.getAssignJobs().getRemovingJobs())); + assertSame(consistentHash.getNode("node1"), node1); + assertNull(consistentHash.getNode("node2")); + consistentHash.removeNode(node1.getIdentity()); + assertEquals(3, consistentHash.getDispatchJobCache().size()); + } + +} + diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/scheduler/ManagerWorkerPoolTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/scheduler/ManagerWorkerPoolTest.java new file mode 100644 index 0000000..b15d192 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/scheduler/ManagerWorkerPoolTest.java @@ -0,0 +1,108 @@ +/* + * 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.manager.scheduler; + +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.concurrent.CountDownLatch; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.hertzbeat.common.concurrent.AdmissionMode; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Test for {@link ManagerWorkerPool}. + */ +class ManagerWorkerPoolTest { + + private ManagerWorkerPool managerWorkerPool; + + @AfterEach + void tearDown() throws Exception { + if (managerWorkerPool != null) { + managerWorkerPool.destroy(); + } + } + + @Test + void testExecuteJobRunsOnVirtualThread() throws Exception { + managerWorkerPool = new ManagerWorkerPool(); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + + managerWorkerPool.executeJob(() -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void testExecuteJobRejectsWhenConcurrencyLimitReached() throws Exception { + VirtualThreadProperties properties = new VirtualThreadProperties( + true, + VirtualThreadProperties.PoolProperties.collectorDefaults(), + VirtualThreadProperties.PoolProperties.commonDefaults(), + new VirtualThreadProperties.PoolProperties(AdmissionMode.LIMIT_AND_REJECT, 1), + VirtualThreadProperties.AlerterProperties.defaults(), + VirtualThreadProperties.PoolProperties.warehouseDefaults(), + VirtualThreadProperties.AsyncProperties.defaults()); + managerWorkerPool = new ManagerWorkerPool(properties); + + CountDownLatch started = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + managerWorkerPool.executeJob(() -> { + started.countDown(); + try { + release.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + assertTrue(started.await(5, TimeUnit.SECONDS)); + + try { + assertThrows(RejectedExecutionException.class, () -> managerWorkerPool.executeJob(() -> { + })); + } finally { + release.countDown(); + } + } + + @Test + void testExecuteLongRunningRunsOnPlatformThread() throws Exception { + managerWorkerPool = new ManagerWorkerPool(); + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(true); + + managerWorkerPool.executeLongRunning(() -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertFalse(virtualThread.get()); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/scheduler/netty/ManageServerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/scheduler/netty/ManageServerTest.java new file mode 100644 index 0000000..16c8597 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/scheduler/netty/ManageServerTest.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.manager.scheduler.netty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.hertzbeat.alert.calculate.CollectorAlertHandler; +import org.apache.hertzbeat.common.concurrent.BackgroundTaskExecutor; +import org.apache.hertzbeat.common.config.VirtualThreadProperties; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.manager.scheduler.CollectorJobScheduler; +import org.apache.hertzbeat.manager.scheduler.SchedulerProperties; +import org.apache.hertzbeat.remoting.RemotingServer; +import org.junit.jupiter.api.AfterEach; +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.test.util.ReflectionTestUtils; + +/** + * Test case for {@link ManageServer}. + */ +@ExtendWith(MockitoExtension.class) +class ManageServerTest { + + @Mock + private CollectorJobScheduler collectorJobScheduler; + + @Mock + private BackgroundTaskExecutor commonThreadPool; + + @Mock + private CollectorAlertHandler collectorAlertHandler; + + @Mock + private CommonDataQueue commonDataQueue; + + private ManageServer manageServer; + + @BeforeEach + void setUp() { + manageServer = new ManageServer(schedulerProperties(), collectorJobScheduler, commonThreadPool, + collectorAlertHandler, commonDataQueue, new VirtualThreadProperties()); + ReflectionTestUtils.setField(manageServer, "remotingServer", mock(RemotingServer.class)); + } + + @AfterEach + void tearDown() { + if (manageServer != null) { + manageServer.shutdown(); + } + } + + @Test + void dispatchChannelHealthCheckRunsOnVirtualThread() throws Exception { + Channel channel = mock(Channel.class); + when(channel.isActive()).thenReturn(false); + when(channel.closeFuture()).thenReturn(mock(ChannelFuture.class)); + clientChannelTable().put("collector-1", channel); + + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean virtualThread = new AtomicBoolean(false); + org.mockito.Mockito.doAnswer(invocation -> { + virtualThread.set(Thread.currentThread().isVirtual()); + latch.countDown(); + return null; + }).when(collectorJobScheduler).collectorGoOffline(anyString()); + + manageServer.dispatchChannelHealthCheck(); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(virtualThread.get()); + } + + @Test + void dispatchChannelHealthCheckDoesNotRunConcurrently() throws Exception { + Channel channel = mock(Channel.class); + clientChannelTable().put("collector-1", channel); + + CountDownLatch firstStarted = new CountDownLatch(1); + CountDownLatch releaseFirst = new CountDownLatch(1); + CountDownLatch secondStarted = new CountDownLatch(1); + AtomicInteger concurrent = new AtomicInteger(); + AtomicInteger maxConcurrent = new AtomicInteger(); + AtomicInteger invocations = new AtomicInteger(); + org.mockito.Mockito.doAnswer(invocation -> { + int running = concurrent.incrementAndGet(); + maxConcurrent.accumulateAndGet(running, Math::max); + int currentInvocation = invocations.incrementAndGet(); + if (currentInvocation == 1) { + firstStarted.countDown(); + releaseFirst.await(5, TimeUnit.SECONDS); + } else if (currentInvocation == 2) { + secondStarted.countDown(); + } + concurrent.decrementAndGet(); + return true; + }).when(channel).isActive(); + + manageServer.dispatchChannelHealthCheck(); + assertTrue(firstStarted.await(5, TimeUnit.SECONDS)); + + manageServer.dispatchChannelHealthCheck(); + assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS)); + + releaseFirst.countDown(); + assertTrue(secondStarted.await(5, TimeUnit.SECONDS)); + assertEquals(1, maxConcurrent.get()); + } + + @SuppressWarnings("unchecked") + private Map clientChannelTable() { + return (Map) ReflectionTestUtils.getField(manageServer, "clientChannelTable"); + } + + private SchedulerProperties schedulerProperties() { + SchedulerProperties schedulerProperties = new SchedulerProperties(); + SchedulerProperties.ServerProperties serverProperties = new SchedulerProperties.ServerProperties(); + serverProperties.setPort(1158); + serverProperties.setIdleStateEventTriggerTime(100); + schedulerProperties.setServer(serverProperties); + return schedulerProperties; + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/script/YamlCheckScript.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/script/YamlCheckScript.java new file mode 100644 index 0000000..67c00f0 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/script/YamlCheckScript.java @@ -0,0 +1,98 @@ +/* + * 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.manager.script; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.util.JexlCheckerUtil; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Stream; + +/** + * Yaml check script + */ +public class YamlCheckScript { + + static final String YML_PATH = "src/main/resources/define"; + + @Test + public void checkYaml() throws IOException { + Path definePath = Paths.get(YML_PATH); + if (!Files.exists(definePath)) { + throw new IllegalStateException("Define directory not found: " + YML_PATH); + } + // check keywords/start char/space + try (Stream paths = Files.walk(definePath)) { + paths.filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".yml")) + .forEach(this::validateYmlFile); + } + } + + private void validateYmlFile(Path filePath) { + var yaml = new Yaml(); + Job app; + try { + app = yaml.loadAs(Files.readString(filePath), Job.class); + } catch (Exception e) { + throw new IllegalArgumentException("parse yml error in file " + filePath.getFileName() + ": " + e.getMessage()); + } + if (app == null) { + throw new IllegalArgumentException("Failed to load Job from file: " + filePath.getFileName()); + } + try { + validateJexl(app.getMetrics()); + } catch (Exception e) { + System.out.printf("file: %s , msg: %s%n", filePath.getFileName(), e.getMessage()); + } + } + + private void validateJexl(List metrics) { + if (null == metrics || metrics.isEmpty()) { + return; + } + for (Metrics metric : metrics) { + if (null == metric.getFields() || metric.getFields().isEmpty()) { + continue; + } + for (Metrics.Field field : metric.getFields()) { + if (null == field || StringUtils.isBlank(field.getField())) { + continue; + } + if (JexlCheckerUtil.verifyKeywords(field.getField())) { + throw new IllegalArgumentException("check jexl keywords failed. field:" + field.getField()); + } + if (JexlCheckerUtil.verifyStartCharacter(field.getField())) { + throw new IllegalArgumentException("check jexl start char failed. field:" + field.getField()); + } + if (JexlCheckerUtil.verifySpaces(field.getField())) { + throw new IllegalArgumentException("check jexl spaces failed. field:" + field.getField()); + } + } + } + + } +} \ No newline at end of file diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AccountServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AccountServiceTest.java new file mode 100644 index 0000000..5f1d95d --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AccountServiceTest.java @@ -0,0 +1,558 @@ +/* + * 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.manager.service; + +import com.usthe.sureness.provider.DefaultAccount; +import com.usthe.sureness.provider.SurenessAccount; +import com.usthe.sureness.provider.SurenessAccountProvider; +import com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.JsonWebTokenUtil; +import com.usthe.sureness.util.Md5Util; +import com.usthe.sureness.util.SurenessContextHolder; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.MalformedJwtException; +import org.apache.hertzbeat.common.entity.manager.AuthToken; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.hertzbeat.manager.dao.AuthTokenDao; +import org.apache.hertzbeat.manager.pojo.dto.LoginDto; +import org.apache.hertzbeat.manager.pojo.dto.RefreshTokenResponse; +import org.apache.hertzbeat.manager.service.impl.AccountServiceImpl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.test.util.ReflectionTestUtils; + +import javax.naming.AuthenticationException; +import java.time.LocalDateTime; +import java.util.Collections; +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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +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.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * test case for {@link AccountServiceImpl} + */ + +class AccountServiceTest { + + private AccountServiceImpl accountService; + + private SurenessAccountProvider accountProvider; + + private AuthTokenDao authTokenDao; + + private final String identifier = "admin"; + private final String password = "hertzbeat"; + private final String salt = "salt1"; + private final List roles = List.of("admin"); + + private final String jwt = """ + CyaFv0bwq2Eik0jdrKUtsA6bx3sDJeFV643R + LnfKefTjsIfJLBa2YkhEqEGtcHDTNe4CU6+9 + 8tVt4bisXQ13rbN0oxhUZR73M6EByXIO+SV5 + dKhaX0csgOCTlCxq20yhmUea6H6JIpSE2Rwp + """; + + @BeforeEach + void setUp() { + + accountProvider = mock(SurenessAccountProvider.class); + authTokenDao = mock(AuthTokenDao.class); + accountService = new AccountServiceImpl(accountProvider); + ReflectionTestUtils.setField(accountService, "authTokenDao", authTokenDao); + + JsonWebTokenUtil.setDefaultSecretKey(jwt); + } + + @Test + void testAuthGetTokenWithValidAccount() throws AuthenticationException { + + SurenessAccount account = DefaultAccount.builder("app1") + .setPassword(Md5Util.md5(password + salt)) + .setSalt(salt) + .setOwnRoles(roles) + .setDisabledAccount(Boolean.FALSE) + .setExcessiveAttempts(Boolean.FALSE) + .build(); + LoginDto loginDto = LoginDto.builder() + .credential(password) + .identifier(identifier) + .build(); + + when(accountProvider.loadAccount(identifier)).thenReturn(account); + + Map response = accountService.authGetToken(loginDto); + + assertNotNull(response); + assertNotNull(response.get("token")); + assertNotNull(response.get("refreshToken")); + assertNotNull(response.get("role")); + assertEquals(JsonUtil.toJson(roles), response.get("role")); + + } + + @Test + void testAuthGetTokenWithInvalidAccount() { + + String identifier = "user1"; + String password = "wrongPassword"; + LoginDto loginDto = LoginDto.builder() + .credential(password) + .identifier(identifier) + .build(); + + when(accountProvider.loadAccount(identifier)).thenReturn(null); + + Assertions.assertThrows( + AuthenticationException.class, + () -> accountService.authGetToken(loginDto) + ); + } + + @Test + void testRefreshTokenWithValidToken() throws Exception { + + String userId = "admin"; + String refreshToken = JsonWebTokenUtil.issueJwt(userId, 3600L, Collections.singletonMap("refresh", true)); + + SurenessAccount account = DefaultAccount.builder("app1") + .setPassword(Md5Util.md5(password + salt)) + .setSalt(salt) + .setOwnRoles(roles) + .setDisabledAccount(Boolean.FALSE) + .setExcessiveAttempts(Boolean.FALSE) + .build(); + when(accountProvider.loadAccount(userId)).thenReturn(account); + + RefreshTokenResponse response = accountService.refreshToken(refreshToken); + + assertNotNull(response); + assertNotNull(response.getToken()); + assertNotNull(response.getRefreshToken()); + Claims accessClaims = JsonWebTokenUtil.parseJwt(response.getToken()); + Claims refreshClaims = JsonWebTokenUtil.parseJwt(response.getRefreshToken()); + assertNull(accessClaims.get("refresh", Boolean.class)); + assertEquals(Boolean.TRUE, refreshClaims.get("refresh", Boolean.class)); + } + + @Test + void testRefreshTokenRejectsAccessToken() { + String userId = "admin"; + String accessToken = JsonWebTokenUtil.issueJwt(userId, 3600L, roles); + + Assertions.assertThrows( + AuthenticationException.class, + () -> accountService.refreshToken(accessToken) + ); + } + + @Test + void testRefreshTokenRejectsDisabledAccount() { + String userId = "admin"; + String refreshToken = JsonWebTokenUtil.issueJwt(userId, 3600L, Collections.singletonMap("refresh", true)); + SurenessAccount account = DefaultAccount.builder("app1") + .setPassword(Md5Util.md5(password + salt)) + .setSalt(salt) + .setOwnRoles(roles) + .setDisabledAccount(Boolean.TRUE) + .setExcessiveAttempts(Boolean.FALSE) + .build(); + when(accountProvider.loadAccount(userId)).thenReturn(account); + + Assertions.assertThrows( + AuthenticationException.class, + () -> accountService.refreshToken(refreshToken) + ); + } + + @Test + void testGenerateTokenCannotRefresh() throws Exception { + SurenessAccount account = DefaultAccount.builder("app1") + .setPassword(Md5Util.md5(password + salt)) + .setSalt(salt) + .setOwnRoles(roles) + .setDisabledAccount(Boolean.FALSE) + .setExcessiveAttempts(Boolean.FALSE) + .build(); + when(accountProvider.loadAccount(identifier)).thenReturn(account); + when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> invocation.getArgument(0)); + SubjectSum subjectSum = mock(SubjectSum.class); + when(subjectSum.getPrincipal()).thenReturn(identifier); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + String token = accountService.generateToken(null, null); + + assertNull(JsonWebTokenUtil.parseJwt(token).get("refresh", Boolean.class)); + Assertions.assertThrows( + AuthenticationException.class, + () -> accountService.refreshToken(token) + ); + } + } + + @Test + void testGenerateTokenRejectsDisabledAccount() { + SurenessAccount account = DefaultAccount.builder("app1") + .setPassword(Md5Util.md5(password + salt)) + .setSalt(salt) + .setOwnRoles(roles) + .setDisabledAccount(Boolean.TRUE) + .setExcessiveAttempts(Boolean.FALSE) + .build(); + when(accountProvider.loadAccount(identifier)).thenReturn(account); + SubjectSum subjectSum = mock(SubjectSum.class); + when(subjectSum.getPrincipal()).thenReturn(identifier); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + Assertions.assertThrows( + AuthenticationException.class, + () -> accountService.generateToken(null, null) + ); + } + } + + @Test + void testRefreshTokenWithInvalidToken() { + + String refreshToken = "invalidToken"; + + Assertions.assertThrows( + MalformedJwtException.class, + () -> accountService.refreshToken(refreshToken) + ); + } + + // ==================== Token Management Tests ==================== + + @Test + void testGenerateTokenPersistsToDb() throws Exception { + SurenessAccount account = buildActiveAccount(); + when(accountProvider.loadAccount(identifier)).thenReturn(account); + when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> invocation.getArgument(0)); + SubjectSum subjectSum = mockAdminSubject(identifier); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + String token = accountService.generateToken("My Token", null); + + assertNotNull(token); + ArgumentCaptor captor = ArgumentCaptor.forClass(AuthToken.class); + verify(authTokenDao).save(captor.capture()); + AuthToken saved = captor.getValue(); + assertEquals("My Token", saved.getName()); + assertNotNull(saved.getTokenHash()); + assertNotNull(saved.getTokenMask()); + assertEquals((byte) 0, saved.getStatus()); + assertEquals(identifier, saved.getCreator()); + assertNull(saved.getExpireTime()); + } + } + + @Test + void testGenerateTokenNullName() throws Exception { + SurenessAccount account = buildActiveAccount(); + when(accountProvider.loadAccount(identifier)).thenReturn(account); + when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> invocation.getArgument(0)); + SubjectSum subjectSum = mockAdminSubject(identifier); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + accountService.generateToken(null, null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuthToken.class); + verify(authTokenDao).save(captor.capture()); + assertNull(captor.getValue().getName()); + } + } + + @Test + void testGenerateTokenWithExpiration() throws Exception { + SurenessAccount account = buildActiveAccount(); + when(accountProvider.loadAccount(identifier)).thenReturn(account); + when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> invocation.getArgument(0)); + SubjectSum subjectSum = mockAdminSubject(identifier); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + LocalDateTime before = LocalDateTime.now(); + accountService.generateToken("Expiring Token", 7200L); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuthToken.class); + verify(authTokenDao).save(captor.capture()); + AuthToken saved = captor.getValue(); + assertNotNull(saved.getExpireTime()); + assertTrue(saved.getExpireTime().isAfter(before.plusSeconds(7100))); + } + } + + @Test + void testGenerateTokenHasManagedClaim() throws Exception { + SurenessAccount account = buildActiveAccount(); + when(accountProvider.loadAccount(identifier)).thenReturn(account); + when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> invocation.getArgument(0)); + SubjectSum subjectSum = mockAdminSubject(identifier); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + String token = accountService.generateToken("test", null); + + Claims claims = JsonWebTokenUtil.parseJwt(token); + assertEquals(Boolean.TRUE, claims.get(AccountServiceImpl.CLAIM_MANAGED, Boolean.class)); + } + } + + @Test + void testListTokensForAdmin() { + List expected = List.of( + AuthToken.builder().id(1L).name("Token1").build(), + AuthToken.builder().id(2L).name("Token2").build() + ); + when(authTokenDao.findByStatus((byte) 0)).thenReturn(expected); + SubjectSum subjectSum = mockAdminSubject(identifier); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + List result = accountService.listTokens(); + + assertEquals(2, result.size()); + assertEquals("Token1", result.get(0).getName()); + } + } + + @Test + void testListTokensForCurrentUser() { + List expected = List.of( + AuthToken.builder().id(1L).name("Token1").creator("tom").build() + ); + when(authTokenDao.findByStatusAndCreator((byte) 0, "tom")).thenReturn(expected); + SubjectSum subjectSum = mockUserSubject("tom"); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + List result = accountService.listTokens(); + + assertEquals(1, result.size()); + assertEquals("Token1", result.get(0).getName()); + verify(authTokenDao).findByStatusAndCreator((byte) 0, "tom"); + } + } + + @Test + void testDeleteTokenInvalidatesCache() throws Exception { + AuthToken token = AuthToken.builder().id(1L).tokenHash("hash123").creator(identifier).build(); + when(authTokenDao.findById(1L)).thenReturn(Optional.of(token)); + SubjectSum subjectSum = mockAdminSubject(identifier); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + accountService.deleteToken(1L); + } + + verify(authTokenDao).deleteById(1L); + } + + @Test + void testDeleteTokenRejectsNonOwner() { + AuthToken token = AuthToken.builder().id(1L).tokenHash("hash123").creator("admin").build(); + when(authTokenDao.findById(1L)).thenReturn(Optional.of(token)); + SubjectSum subjectSum = mockUserSubject("tom"); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + Assertions.assertThrows(AuthenticationException.class, () -> accountService.deleteToken(1L)); + } + } + + @Test + void testCheckTokenStatusActive() throws Exception { + SurenessAccount account = buildActiveAccount(); + when(accountProvider.loadAccount(identifier)).thenReturn(account); + when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> invocation.getArgument(0)); + SubjectSum subjectSum = mock(SubjectSum.class); + when(subjectSum.getPrincipal()).thenReturn(identifier); + + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + + String token = accountService.generateToken("test", null); + when(authTokenDao.existsByTokenHashAndStatus(any(String.class), eq((byte) 0))).thenReturn(true); + + String result = accountService.checkTokenStatus(token); + + assertNull(result); + } + } + + @Test + void testCheckTokenStatusRevoked() { + when(authTokenDao.existsByTokenHashAndStatus(any(String.class), eq((byte) 0))).thenReturn(false); + + String result = accountService.checkTokenStatus("some-revoked-token"); + + assertEquals("Token has been revoked", result); + } + + @Test + void testCheckTokenStatusUsesCache() { + when(authTokenDao.existsByTokenHashAndStatus(any(String.class), eq((byte) 0))).thenReturn(true); + + // First call - hits DB + assertNull(accountService.checkTokenStatus("cached-token")); + // Second call - should use cache, no additional DB call + assertNull(accountService.checkTokenStatus("cached-token")); + + verify(authTokenDao, times(1)).existsByTokenHashAndStatus(any(String.class), eq((byte) 0)); + } + + @Test + void testCheckManagedTokenAccessValid() { + when(accountProvider.loadAccount(identifier)).thenReturn(buildActiveAccount()); + + String result = accountService.checkManagedTokenAccess(identifier, List.of("admin")); + + assertNull(result); + } + + @Test + void testCheckManagedTokenAccessRejectsDisabledAccount() { + SurenessAccount account = DefaultAccount.builder("app1") + .setPassword(Md5Util.md5(password + salt)) + .setSalt(salt) + .setOwnRoles(roles) + .setDisabledAccount(Boolean.TRUE) + .setExcessiveAttempts(Boolean.FALSE) + .build(); + when(accountProvider.loadAccount(identifier)).thenReturn(account); + + String result = accountService.checkManagedTokenAccess(identifier, List.of("admin")); + + assertEquals("Token owner account is no longer valid", result); + } + + @Test + void testCheckManagedTokenAccessRejectsOutdatedRoles() { + SurenessAccount account = DefaultAccount.builder("app1") + .setPassword(Md5Util.md5(password + salt)) + .setSalt(salt) + .setOwnRoles(List.of("guest")) + .setDisabledAccount(Boolean.FALSE) + .setExcessiveAttempts(Boolean.FALSE) + .build(); + when(accountProvider.loadAccount(identifier)).thenReturn(account); + + String result = accountService.checkManagedTokenAccess(identifier, List.of("admin")); + + assertEquals("Token permissions are outdated", result); + } + + @Test + void testTouchTokenLastUsedTime() { + accountService.touchTokenLastUsedTime("some-token-value"); + + verify(authTokenDao).updateLastUsedTime(any(String.class), any(LocalDateTime.class)); + } + + @Test + void testTouchTokenLastUsedTimeThrottled() { + // First call - should write to DB + accountService.touchTokenLastUsedTime("throttle-token"); + // Second call within 5 minutes - should be throttled + accountService.touchTokenLastUsedTime("throttle-token"); + + verify(authTokenDao, times(1)).updateLastUsedTime(any(String.class), any(LocalDateTime.class)); + } + + @Test + void testTouchTokenLastUsedTimeDifferentTokensNotThrottled() { + accountService.touchTokenLastUsedTime("token-a"); + accountService.touchTokenLastUsedTime("token-b"); + + verify(authTokenDao, times(2)).updateLastUsedTime(any(String.class), any(LocalDateTime.class)); + } + + @Test + void testDeleteTokenCacheInvalidation() throws Exception { + // Setup: token is active and cached + String tokenValue = "token-to-revoke"; + when(authTokenDao.existsByTokenHashAndStatus(any(String.class), eq((byte) 0))).thenReturn(true); + assertNull(accountService.checkTokenStatus(tokenValue)); + + // Now revoke it + AuthToken authToken = AuthToken.builder().id(1L).tokenHash("some-hash").creator(identifier).build(); + when(authTokenDao.findById(1L)).thenReturn(Optional.of(authToken)); + SubjectSum subjectSum = mockAdminSubject(identifier); + try (var mockedStatic = mockStatic(SurenessContextHolder.class)) { + mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum); + accountService.deleteToken(1L); + } + + // After revoke, the next check should hit DB again (cache invalidated for that hash) + // Note: the tokenHash in DB differs from sha256(tokenValue), so this tests cache invalidation path + verify(authTokenDao).findById(1L); + verify(authTokenDao).deleteById(1L); + } + + private SurenessAccount buildActiveAccount() { + return DefaultAccount.builder("app1") + .setPassword(Md5Util.md5(password + salt)) + .setSalt(salt) + .setOwnRoles(roles) + .setDisabledAccount(Boolean.FALSE) + .setExcessiveAttempts(Boolean.FALSE) + .build(); + } + + private SubjectSum mockAdminSubject(String principal) { + SubjectSum subjectSum = mock(SubjectSum.class); + when(subjectSum.getPrincipal()).thenReturn(principal); + when(subjectSum.hasRole("admin")).thenReturn(true); + return subjectSum; + } + + private SubjectSum mockUserSubject(String principal) { + SubjectSum subjectSum = mock(SubjectSum.class); + when(subjectSum.getPrincipal()).thenReturn(principal); + when(subjectSum.hasRole("admin")).thenReturn(false); + return subjectSum; + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AppServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AppServiceTest.java new file mode 100644 index 0000000..56f9c01 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AppServiceTest.java @@ -0,0 +1,206 @@ +/* + * 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.manager.service; + +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.RuntimeParamDefine; +import org.apache.hertzbeat.common.entity.manager.Define; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.manager.dao.DefineDao; +import org.apache.hertzbeat.manager.dao.MonitorDao; +import org.apache.hertzbeat.manager.pojo.dto.ObjectStoreConfigChangeEvent; +import org.apache.hertzbeat.manager.pojo.dto.ObjectStoreDTO; +import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo; +import org.apache.hertzbeat.manager.service.impl.AppServiceImpl; +import org.apache.hertzbeat.manager.service.impl.ObjectStoreConfigServiceImpl; +import org.apache.hertzbeat.warehouse.service.WarehouseService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * Test case for {@link AppService} + */ +@ExtendWith(MockitoExtension.class) +class AppServiceTest { + + @InjectMocks + private AppServiceImpl appService; + + @Mock + private MonitorDao monitorDao; + + @Mock + private DefineDao defineDao; + + @Mock + private WarehouseService warehouseService; + + @Mock + private ObjectStoreConfigServiceImpl objectStoreConfigService; + + @BeforeEach + void setUp() throws Exception { + when(defineDao.findAll()).thenReturn(new ArrayList<>()); + appService.afterPropertiesSet(); + } + + @Test + void getAppParamDefines() { + assertDoesNotThrow(() -> appService.getAppParamDefines("jvm")); + } + + @Test + void getAppParamDefinesShouldNotContainDuplicateFields() { + List paramDefines = appService.getAppParamDefines("nvidia"); + Map fieldCounts = paramDefines.stream() + .collect(Collectors.groupingBy(ParamDefineInfo::getField, Collectors.counting())); + + assertTrue(fieldCounts.values().stream().allMatch(count -> count == 1), + () -> "Duplicate param fields found: " + fieldCounts.entrySet().stream() + .filter(entry -> entry.getValue() > 1) + .map(Map.Entry::getKey) + .collect(Collectors.joining(", "))); + } + + @Test + void getAppDefine() { + assertDoesNotThrow(() -> appService.getAppDefine("jvm")); + assertThrows(IllegalArgumentException.class, () -> appService.getAppDefine("unknown")); + } + + @Test + void getAppDefineMetricNames() { + assertDoesNotThrow(() -> appService.getAppDefineMetricNames("jvm")); + } + + @Test + void getI18nResources() { + assertDoesNotThrow(() -> appService.getI18nResources("en-US")); + } + + @Test + void getAllAppHierarchy() { + when(monitorDao.findMonitorsByAppEquals(anyString())).thenReturn(Collections + .singletonList(Monitor.builder().id(1L).build())); + when(warehouseService.queryMonitorMetricsData(anyLong())).thenReturn(Collections.emptyList()); + assertDoesNotThrow(() -> appService.getAllAppHierarchy("en-US")); + } + + @Test + void appDefineJexl() throws NoSuchMethodException { + Job job = new Job(); + job.setApp("test-app"); + job.setCategory("service"); + job.setName(Map.of("k", "v")); + + List params = new ArrayList<>(); + RuntimeParamDefine hostParam = new RuntimeParamDefine(); + hostParam.setField("host"); + hostParam.setType("host"); + hostParam.setRequired(true); + params.add(hostParam); + + RuntimeParamDefine portParam = new RuntimeParamDefine(); + portParam.setField("port"); + portParam.setType("number"); + portParam.setRequired(true); + portParam.setDefaultValue("8080"); + params.add(portParam); + + job.setParams(params); + + List metrics = new ArrayList<>(); + + Metrics otherMetrics = new Metrics(); + otherMetrics.setName("details"); + otherMetrics.setPriority((byte) 0); + otherMetrics.setProtocol("http"); + + List fields = new ArrayList<>(); + fields.add(Metrics.Field.builder().field("size").build()); + otherMetrics.setFields(fields); + + metrics.add(otherMetrics); + job.setMetrics(metrics); + + Method verifyMethod = AppServiceImpl.class.getDeclaredMethod("verifyDefineAppContent", Job.class, boolean.class); + verifyMethod.setAccessible(true); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + try { + verifyMethod.invoke(appService, job, false); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw e.getCause(); + } + throw new RuntimeException(e.getCause()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + assertTrue(exception.getMessage().contains("prohibited keywords")); + } + + @Test + void testRefreshStore() throws Exception { + List defines = new ArrayList<>(); + Define define = Define.builder() + .app("mysql") + .content(""" + app: mysql + params: + - field: host_test + name: + en-US: Target Host + type: host + required: true""") + .build(); + defines.add(define); + + when(defineDao.findAll()).thenReturn(defines); + + ObjectStoreDTO objectStoreDTO = new ObjectStoreDTO<>(); + objectStoreDTO.setType(ObjectStoreDTO.Type.DATABASE); + ObjectStoreConfigChangeEvent objectStoreConfigChangeEvent = new ObjectStoreConfigChangeEvent(objectStoreDTO); + appService.onObjectStoreConfigChange(objectStoreConfigChangeEvent); + + List appParamDefines = appService.getAppParamDefines(define.getApp()); + assertNotNull(appParamDefines); + assertTrue(appParamDefines.stream().anyMatch(t -> t.getField().equals("host_test"))); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/BulletinServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/BulletinServiceTest.java new file mode 100644 index 0000000..dd97683 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/BulletinServiceTest.java @@ -0,0 +1,170 @@ +/* + * 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.manager.service; + +import org.apache.hertzbeat.common.entity.manager.Bulletin; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.manager.dao.BulletinDao; +import org.apache.hertzbeat.manager.service.impl.BulletinServiceImpl; +import org.apache.hertzbeat.warehouse.store.realtime.RealTimeDataReader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * Test case for {@link BulletinService} + */ +@ExtendWith(MockitoExtension.class) +public class BulletinServiceTest { + @InjectMocks + private BulletinServiceImpl bulletinService; + + @Mock + private BulletinDao bulletinDao; + + @Mock + private MonitorService monitorService; + + @Mock + private RealTimeDataReader realTimeDataReader; + + @Test + public void testValidate() throws Exception { + assertThrows(IllegalArgumentException.class, () -> { + bulletinService.validate(null); + }); + + assertThrows(IllegalArgumentException.class, () -> { + bulletinService.validate(new Bulletin()); + }); + + assertThrows(IllegalArgumentException.class, () -> { + Bulletin obj = new Bulletin(); + obj.setApp("app"); + bulletinService.validate(obj); + }); + + assertThrows(IllegalArgumentException.class, () -> { + Map> fields = new HashMap>(); + fields.put("field1", null); + + Bulletin obj = new Bulletin(); + obj.setApp("app"); + obj.setFields(fields); + bulletinService.validate(obj); + }); + + assertDoesNotThrow(() -> { + Map> fields = new HashMap>(); + fields.put("field1", null); + + List ids = new ArrayList(); + ids.add((long) 1); + + Bulletin obj = new Bulletin(); + obj.setApp("app"); + obj.setFields(fields); + obj.setMonitorIds(ids); + bulletinService.validate(obj); + }); + } + + @Test + public void testAddBulletin() throws Exception { + Map> fields = new HashMap>(); + fields.put("field1", null); + + Bulletin bulletinDto = new Bulletin(); + bulletinDto.setApp("app"); + bulletinDto.setFields(fields); + + assertDoesNotThrow(() -> { + bulletinService.addBulletin(bulletinDto); + }); + } + + @Test + public void testGetBulletins() throws Exception { + Bulletin bulletin = new Bulletin(); + bulletin.setId((long) 1); + + PageRequest pageRequest = PageRequest.of(0, 10); + + List content = Collections.singletonList(bulletin); + long total = 1; + + Page items = new PageImpl(content, pageRequest, total); + + when(bulletinDao.findAll(any(Specification.class), any(PageRequest.class))).thenReturn(items); + assertNotNull(bulletinService.getBulletins(null, null, null)); + } + + @Test + public void testBuildBulletinMetricsData() throws Exception { + List ids = new ArrayList(); + ids.add((long) 1); + Bulletin bulletin = new Bulletin(); + bulletin.setId((long) 1); + bulletin.setMonitorIds(ids); + HashMap> fields = new HashMap<>(); + fields.put("1", List.of("1", "2")); + bulletin.setFields(fields); + + Monitor monitor = new Monitor(); + + when(bulletinDao.findById(any(Long.class))).thenReturn(java.util.Optional.of(bulletin)); + when(realTimeDataReader.getCurrentMetricsData(any(), any(String.class))).thenReturn(null); + + when(monitorService.getMonitor(any(Long.class))).thenReturn(null); + assertTrue(bulletinService.buildBulletinMetricsData(any(Long.class)).getContent().isEmpty()); + + when(monitorService.getMonitor(1L)).thenReturn(monitor); + assertFalse(bulletinService.buildBulletinMetricsData(2L).getContent().isEmpty()); + + } + + @Test + public void testGetBulletinById() throws Exception { + Bulletin bulletin = new Bulletin(); + bulletin.setId((long) 1); + + when(bulletinDao.findById((long) 1)).thenReturn(java.util.Optional.of(bulletin)); + assertEquals(bulletin, bulletinService.getBulletinById((long) 1).get()); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/CollectorServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/CollectorServiceTest.java new file mode 100644 index 0000000..f5dc209 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/CollectorServiceTest.java @@ -0,0 +1,115 @@ +/* + * 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.manager.service; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +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.when; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.hertzbeat.common.entity.manager.Collector; +import org.apache.hertzbeat.common.support.exception.CommonException; +import org.apache.hertzbeat.manager.dao.CollectorDao; +import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao; +import org.apache.hertzbeat.manager.scheduler.ConsistentHash; +import org.apache.hertzbeat.manager.scheduler.netty.ManageServer; +import org.apache.hertzbeat.manager.service.impl.CollectorServiceImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationContext; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; + +/** + * Test case for {@link CollectorService} + */ +@ExtendWith(MockitoExtension.class) +public class CollectorServiceTest { + + @Spy + @InjectMocks + private CollectorServiceImpl collectorService; + + @Mock + private CollectorDao collectorDao; + + @Mock + private ConsistentHash consistentHash; + + @Mock + private CollectorMonitorBindDao collectorMonitorBindDao; + + @Mock + private ManageServer manageServer; + + @Mock + private ApplicationContext applicationContext; + + + @Test + public void getCollectors() { + when(collectorDao.findAll(any(Specification.class), eq(PageRequest.of(1, 1)))).thenReturn(Page.empty()); + assertDoesNotThrow(() -> collectorService.getCollectors("test", 1, 1)); + } + + @Test + public void deleteRegisteredCollector() { + List collectors = new ArrayList<>(); + collectors.add("test"); + collectorService.deleteRegisteredCollector(collectors); + } + + @Test + public void hasCollector() { + when(collectorDao.findCollectorByName("test")).thenReturn(Optional.empty()); + assertFalse(collectorService.hasCollector("test")); + } + + @Test + public void testGenerateCollectorDeployInfo() { + when(collectorDao.findCollectorByName("test")).thenReturn(Optional.of(new Collector())); + assertThrows(CommonException.class, ()->{ + collectorService.generateCollectorDeployInfo("test"); + }); + } + + @Test + public void testMakeCollectorsOffline() { + assertDoesNotThrow(() -> { + collectorService.makeCollectorsOffline(new ArrayList<>()); + }); + } + + @Test + public void testMakeCollectorsOnline() { + assertDoesNotThrow(() -> { + collectorService.makeCollectorsOnline(new ArrayList<>()); + }); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ConfigServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ConfigServiceTest.java new file mode 100644 index 0000000..ea43185 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ConfigServiceTest.java @@ -0,0 +1,107 @@ +/* + * 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.manager.service; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.ArrayList; +import java.util.List; +import org.apache.hertzbeat.base.service.GeneralConfigService; +import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum; +import org.apache.hertzbeat.common.entity.dto.MailServerConfig; +import org.apache.hertzbeat.manager.pojo.dto.ObjectStoreDTO; +import org.apache.hertzbeat.manager.pojo.dto.TemplateConfig; +import org.apache.hertzbeat.manager.service.impl.ConfigServiceImpl; +import org.apache.hertzbeat.manager.service.impl.MailGeneralConfigServiceImpl; +import org.apache.hertzbeat.manager.service.impl.ObjectStoreConfigServiceImpl; +import org.apache.hertzbeat.manager.service.impl.TemplateConfigServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link ConfigService} + */ +@ExtendWith(MockitoExtension.class) +public class ConfigServiceTest { + + @InjectMocks + private ConfigServiceImpl configService; + @Mock + private ObjectStoreConfigServiceImpl objectStoreConfigService; + @Mock + private TemplateConfigServiceImpl templateConfigService; + @Mock + private MailGeneralConfigServiceImpl mailGeneralConfigService; + + @BeforeEach + public void setUp() { + List generalConfigServices = new ArrayList<>(); + when(objectStoreConfigService.type()).thenReturn(GeneralConfigTypeEnum.oss.name()); + when(templateConfigService.type()).thenReturn(GeneralConfigTypeEnum.template.name()); + when(mailGeneralConfigService.type()).thenReturn(GeneralConfigTypeEnum.email.name()); + generalConfigServices.add(objectStoreConfigService); + generalConfigServices.add(templateConfigService); + generalConfigServices.add(mailGeneralConfigService); + configService = new ConfigServiceImpl(generalConfigServices); + } + + @Test + public void testSaveConfig() { + configService.saveConfig(GeneralConfigTypeEnum.oss.name(), new ObjectStoreDTO<>()); + verify(objectStoreConfigService, times(1)).saveConfig(any(ObjectStoreDTO.class)); + + configService.saveConfig(GeneralConfigTypeEnum.email.name(), new MailServerConfig()); + verify(mailGeneralConfigService, times(1)).saveConfig(any(MailServerConfig.class)); + } + + @Test + public void testGetConfig() { + ObjectStoreDTO ossConfig = new ObjectStoreDTO<>(); + when(objectStoreConfigService.getConfig()).thenReturn(ossConfig); + assertNotNull(configService.getConfig(GeneralConfigTypeEnum.oss.name())); + + MailServerConfig emailNoticeSender = new MailServerConfig(); + when(mailGeneralConfigService.getConfig()).thenReturn(emailNoticeSender); + configService.getConfig(GeneralConfigTypeEnum.email.name()); + verify(mailGeneralConfigService, times(1)).getConfig(); + } + + @Test + public void testUpdateTemplateAppConfig() { + TemplateConfig templateConfig = new TemplateConfig(); + when(templateConfigService.getConfig()).thenReturn(templateConfig); + configService.updateTemplateAppConfig("custom", new TemplateConfig.AppTemplate()); + + verify(templateConfigService, times(1)).getConfig(); + verify(templateConfigService, times(1)).saveConfig(templateConfig); + } + + @Test + public void testException() { + assertThrows(IllegalArgumentException.class, () -> configService.saveConfig("test", new ObjectStoreDTO<>())); + assertThrows(IllegalArgumentException.class, () -> configService.getConfig("test2"), "Not supported this config type: test2"); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/DefaultPluginRunnerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/DefaultPluginRunnerTest.java new file mode 100644 index 0000000..84d987b --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/DefaultPluginRunnerTest.java @@ -0,0 +1,71 @@ +/* + * 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.manager.service; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.doAnswer; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static org.mockito.Mockito.any; + +import org.apache.hertzbeat.manager.service.impl.DefaultPluginRunner; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link DefaultPluginRunner} + */ +@ExtendWith(MockitoExtension.class) +public class DefaultPluginRunnerTest { + @Mock + private PluginService pluginService; + @InjectMocks + private DefaultPluginRunner defaultPluginRunner; + + @Test + public void testPluginExecute() { + assertDoesNotThrow(() -> { + doAnswer(invocation -> { + // no nothing + return null; + }).when(pluginService).pluginExecute(any(Class.class), any(Consumer.class)); + doAnswer(invocation -> { + // no nothing + return null; + }).when(pluginService).pluginExecute(any(Class.class), any(BiConsumer.class)); + defaultPluginRunner.pluginExecute(String.class, (s) -> {}); + defaultPluginRunner.pluginExecute(String.class, (a, b)->{}); + }); + + assertDoesNotThrow(() -> { + doAnswer(invocation -> { + throw new RuntimeException("sample"); + }).when(pluginService).pluginExecute(any(Class.class), any(Consumer.class)); + doAnswer(invocation -> { + throw new RuntimeException("sample"); + }).when(pluginService).pluginExecute(any(Class.class), any(BiConsumer.class)); + defaultPluginRunner.pluginExecute(String.class, (s) -> {}); + defaultPluginRunner.pluginExecute(String.class, (a, b)->{}); + }); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ExcelImExportServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ExcelImExportServiceTest.java new file mode 100644 index 0000000..f87adfe --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ExcelImExportServiceTest.java @@ -0,0 +1,216 @@ +/* + * 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.manager.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.apache.hertzbeat.manager.config.ManagerSseManager; +import org.apache.hertzbeat.manager.pojo.dto.MonitorDto; +import org.apache.hertzbeat.manager.service.impl.AbstractImExportServiceImpl; +import org.apache.hertzbeat.manager.service.impl.ExcelImExportServiceImpl; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test case for {@link ExcelImExportServiceImpl} + */ + +class ExcelImExportServiceTest { + + @InjectMocks + private ExcelImExportServiceImpl excelImExportService; + + @Mock + private MonitorService monitorService; + + @Mock + private ManagerSseManager managerSseManager; + + @BeforeEach + public void setUp() { + + MockitoAnnotations.openMocks(this); + } + + @Test + public void testParseImport() throws IOException { + + Workbook workbook = new XSSFWorkbook(); + var sheet = workbook.createSheet(); + var headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("name"); + headerRow.createCell(1).setCellValue("app"); + headerRow.createCell(2).setCellValue("host"); + var dataRow = sheet.createRow(1); + dataRow.createCell(0).setCellValue("Monitor1"); + dataRow.createCell(1).setCellValue("App1"); + dataRow.createCell(2).setCellValue("Host1"); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + workbook.write(bos); + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + + List result = excelImExportService.parseImport(bis); + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("Monitor1", result.get(0).getMonitor().getName()); + } + + @Test + public void testWriteOs() throws IOException { + + AbstractImExportServiceImpl.MonitorDTO monitorDTO = new AbstractImExportServiceImpl.MonitorDTO(); + monitorDTO.setName("Monitor1"); + monitorDTO.setApp("App1"); + monitorDTO.setHost("Host1"); + monitorDTO.setIntervals(10); + monitorDTO.setStatus((byte) 1); + monitorDTO.setLabels(Map.of("env", "prod")); + + AbstractImExportServiceImpl.ParamDTO paramDTO = new AbstractImExportServiceImpl.ParamDTO(); + paramDTO.setField("field1"); + paramDTO.setValue("value1"); + paramDTO.setType((byte) 1); + + AbstractImExportServiceImpl.ExportMonitorDTO exportMonitorDTO = new AbstractImExportServiceImpl.ExportMonitorDTO(); + exportMonitorDTO.setMonitor(monitorDTO); + exportMonitorDTO.setParams(List.of(paramDTO)); + + List monitorList = List.of(exportMonitorDTO); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + excelImExportService.writeOs(monitorList, bos); + + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + Workbook workbook = WorkbookFactory.create(bis); + var sheet = workbook.getSheetAt(0); + var dataRow = sheet.getRow(1); + + assertEquals("Monitor1", dataRow.getCell(0).getStringCellValue()); + assertEquals("App1", dataRow.getCell(1).getStringCellValue()); + assertEquals("Host1", dataRow.getCell(2).getStringCellValue()); + } + + @Test + public void testImportConfigAllValid() throws IOException { + ByteArrayInputStream is = buildImportStream(List.of("Monitor1", "Monitor2")); + excelImExportService.importConfig("task", is); + verify(monitorService, times(2)).validate(any(MonitorDto.class), eq(false)); + verify(monitorService, times(2)).addMonitor(any(), any(), any(), any()); + verify(managerSseManager).broadcastImportTaskSuccess("task"); + } + + @Test + public void testImportConfigRejectsDuplicateNameInFile() throws IOException { + ByteArrayInputStream is = buildImportStream(List.of("DupMonitor", "DupMonitor")); + assertThrows(IllegalArgumentException.class, () -> excelImExportService.importConfig("task", is)); + verify(monitorService, never()).addMonitor(any(), any(), any(), any()); + verify(managerSseManager, never()).broadcastImportTaskSuccess(any()); + } + + @Test + public void testImportConfigRejectsWholeBatchWhenLaterEntryInvalid() throws IOException { + doNothing().doThrow(new IllegalArgumentException("Monitoring name already exists!")) + .when(monitorService).validate(any(MonitorDto.class), eq(false)); + ByteArrayInputStream is = buildImportStream(List.of("Monitor1", "Monitor2")); + assertThrows(IllegalArgumentException.class, () -> excelImExportService.importConfig("task", is)); + verify(monitorService, never()).addMonitor(any(), any(), any(), any()); + verify(managerSseManager, never()).broadcastImportTaskSuccess(any()); + } + + private ByteArrayInputStream buildImportStream(List names) throws IOException { + try (Workbook workbook = new XSSFWorkbook()) { + var sheet = workbook.createSheet(); + var headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("name"); + headerRow.createCell(1).setCellValue("app"); + headerRow.createCell(2).setCellValue("host"); + for (int i = 0; i < names.size(); i++) { + var dataRow = sheet.createRow(i + 1); + dataRow.createCell(0).setCellValue(names.get(i)); + dataRow.createCell(1).setCellValue("App"); + dataRow.createCell(2).setCellValue("Host"); + dataRow.createCell(4).setCellValue(1); + } + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + workbook.write(bos); + return new ByteArrayInputStream(bos.toByteArray()); + } + } + + @Test + public void testWriteThenParsePreservesAllFields() throws IOException { + + AbstractImExportServiceImpl.MonitorDTO monitorDTO = new AbstractImExportServiceImpl.MonitorDTO(); + monitorDTO.setName("Monitor1"); + monitorDTO.setApp("linux"); + monitorDTO.setHost("Host1"); + monitorDTO.setIntervals(10); + monitorDTO.setStatus((byte) 1); + monitorDTO.setLabels(Map.of("env", "prod")); + monitorDTO.setAnnotations(Map.of("owner", "ops")); + monitorDTO.setScheduleType("cron"); + monitorDTO.setCronExpression("0 0/5 * * * ?"); + + AbstractImExportServiceImpl.ParamDTO paramDTO = new AbstractImExportServiceImpl.ParamDTO(); + paramDTO.setField("host"); + paramDTO.setValue("192.168.1.1"); + paramDTO.setType((byte) 1); + + AbstractImExportServiceImpl.ExportMonitorDTO exportMonitorDTO = new AbstractImExportServiceImpl.ExportMonitorDTO(); + exportMonitorDTO.setMonitor(monitorDTO); + exportMonitorDTO.setParams(List.of(paramDTO)); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + excelImExportService.writeOs(List.of(exportMonitorDTO), bos); + + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + List result = excelImExportService.parseImport(bis); + + assertNotNull(result); + assertEquals(1, result.size()); + AbstractImExportServiceImpl.MonitorDTO parsed = result.get(0).getMonitor(); + assertEquals("prod", parsed.getLabels().get("env")); + assertEquals("ops", parsed.getAnnotations().get("owner")); + assertEquals("cron", parsed.getScheduleType()); + assertEquals("0 0/5 * * * ?", parsed.getCronExpression()); + // verify the shifted param columns still round-trip correctly + assertEquals(1, result.get(0).getParams().size()); + assertEquals("host", result.get(0).getParams().get(0).getField()); + assertEquals("192.168.1.1", result.get(0).getParams().get(0).getValue()); + } + +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/JsonImExportServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/JsonImExportServiceTest.java new file mode 100644 index 0000000..198c89d --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/JsonImExportServiceTest.java @@ -0,0 +1,189 @@ +/* + * 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.manager.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doNothing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Param; +import org.apache.hertzbeat.manager.config.ManagerSseManager; +import org.apache.hertzbeat.manager.service.impl.AbstractImExportServiceImpl; +import org.apache.hertzbeat.manager.service.impl.JsonImExportServiceImpl; +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; + +/** + * Test case for {@link JsonImExportServiceImpl} + */ +@ExtendWith(MockitoExtension.class) +class JsonImExportServiceTest { + + private JsonImExportServiceImpl jsonImExportService; + + @Mock + private MonitorService monitorService; + + @Mock + private ManagerSseManager managerSseManager; + + @BeforeEach + public void setUp() throws Exception { + jsonImExportService = new JsonImExportServiceImpl(); + Field monitorServiceField = jsonImExportService.getClass().getSuperclass().getDeclaredField("monitorService"); + monitorServiceField.setAccessible(true); + monitorServiceField.set(jsonImExportService, monitorService); + Field sseField = jsonImExportService.getClass().getSuperclass().getDeclaredField("managerSseManager"); + sseField.setAccessible(true); + sseField.set(jsonImExportService, managerSseManager); + } + + @Test + void testParseImport() { + String json = "[{\"monitor\":{\"name\":\"Monitor1\",\"app\":\"App1\",\"host\":\"Host1\"}}]"; + ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + + List result = jsonImExportService.parseImport(bis); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("Monitor1", result.get(0).getMonitor().getName()); + } + + @Test + void testParseImportInvalidJson() { + String invalidJson = "invalid json"; + ByteArrayInputStream bis = new ByteArrayInputStream(invalidJson.getBytes(StandardCharsets.UTF_8)); + + assertThrows(RuntimeException.class, () -> jsonImExportService.parseImport(bis)); + } + + @Test + public void testWriteOs() { + AbstractImExportServiceImpl.MonitorDTO monitorDTO = new AbstractImExportServiceImpl.MonitorDTO(); + monitorDTO.setName("Monitor1"); + monitorDTO.setApp("App1"); + monitorDTO.setHost("Host1"); + + AbstractImExportServiceImpl.ExportMonitorDTO exportMonitorDTO = new AbstractImExportServiceImpl.ExportMonitorDTO(); + exportMonitorDTO.setMonitor(monitorDTO); + + List monitorList = List.of(exportMonitorDTO); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + jsonImExportService.writeOs(monitorList, bos); + + String result = bos.toString(StandardCharsets.UTF_8); + assertNotNull(result); + assertTrue(result.contains("Monitor1")); + assertTrue(result.contains("App1")); + } + + @Test + void testType() { + assertEquals("JSON", jsonImExportService.type()); + } + + @Test + void testImportConfig_shouldSetInstanceFromHostAndPortParams() { + String json = "[{\"monitor\":{\"name\":\"test\",\"app\":\"windows\",\"intervals\":6000,\"status\":1}," + + "\"params\":[{\"field\":\"host\",\"type\":1,\"value\":\"localhost\"}," + + "{\"field\":\"port\",\"type\":0,\"value\":\"161\"}]}]"; + + ArgumentCaptor monitorCaptor = ArgumentCaptor.forClass(Monitor.class); + ArgumentCaptor> paramsCaptor = ArgumentCaptor.forClass(List.class); + doNothing().when(monitorService).addMonitor(monitorCaptor.capture(), paramsCaptor.capture(), + org.mockito.Mockito.any(), org.mockito.Mockito.any()); + + ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + jsonImExportService.importConfig("test.json", bis); + + Monitor captured = monitorCaptor.getValue(); + assertEquals("localhost:161", captured.getInstance()); + assertEquals("test", captured.getName()); + assertEquals("windows", captured.getApp()); + + List capturedParams = paramsCaptor.getValue(); + assertNotNull(capturedParams); + assertEquals(2, capturedParams.size()); + } + + @Test + void testImportConfig_shouldSetInstanceWithHostOnly() { + String json = "[{\"monitor\":{\"name\":\"test\",\"app\":\"linux\",\"intervals\":6000,\"status\":1}," + + "\"params\":[{\"field\":\"host\",\"type\":1,\"value\":\"192.168.1.1\"}]}]"; + + ArgumentCaptor monitorCaptor = ArgumentCaptor.forClass(Monitor.class); + doNothing().when(monitorService).addMonitor(monitorCaptor.capture(), + org.mockito.Mockito.any(), org.mockito.Mockito.any(), org.mockito.Mockito.any()); + + ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + jsonImExportService.importConfig("test.json", bis); + + Monitor captured = monitorCaptor.getValue(); + assertEquals("192.168.1.1", captured.getInstance()); + } + + @Test + void testImportConfig_shouldHandleNoHostParam() { + String json = "[{\"monitor\":{\"name\":\"test\",\"app\":\"website\",\"intervals\":6000,\"status\":1}," + + "\"params\":[{\"field\":\"url\",\"type\":1,\"value\":\"http://example.com\"}]}]"; + + ArgumentCaptor monitorCaptor = ArgumentCaptor.forClass(Monitor.class); + doNothing().when(monitorService).addMonitor(monitorCaptor.capture(), + org.mockito.Mockito.any(), org.mockito.Mockito.any(), org.mockito.Mockito.any()); + + ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + jsonImExportService.importConfig("test.json", bis); + + Monitor captured = monitorCaptor.getValue(); + assertEquals(null, captured.getInstance()); + } + + @Test + void testImportConfig_shouldPreserveAnnotationsAndSchedule() { + String json = "[{\"monitor\":{\"name\":\"test\",\"app\":\"linux\",\"intervals\":6000,\"status\":1," + + "\"labels\":{\"env\":\"prod\"},\"annotations\":{\"owner\":\"ops\"}," + + "\"scheduleType\":\"cron\",\"cronExpression\":\"0 0/5 * * * ?\"}," + + "\"params\":[{\"field\":\"host\",\"type\":1,\"value\":\"192.168.1.1\"}]}]"; + + ArgumentCaptor monitorCaptor = ArgumentCaptor.forClass(Monitor.class); + doNothing().when(monitorService).addMonitor(monitorCaptor.capture(), + org.mockito.Mockito.any(), org.mockito.Mockito.any(), org.mockito.Mockito.any()); + + ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + jsonImExportService.importConfig("test.json", bis); + + Monitor captured = monitorCaptor.getValue(); + assertEquals("prod", captured.getLabels().get("env")); + assertEquals("ops", captured.getAnnotations().get("owner")); + assertEquals("cron", captured.getScheduleType()); + assertEquals("0 0/5 * * * ?", captured.getCronExpression()); + } +} diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/LabelServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/LabelServiceTest.java new file mode 100644 index 0000000..29e2d29 --- /dev/null +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/LabelServiceTest.java @@ -0,0 +1,108 @@ +/* + * 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.manager.service; + +import org.apache.hertzbeat.base.service.LabelService; +import org.junit.jupiter.api.Assertions; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.hertzbeat.common.entity.manager.Label; +import org.apache.hertzbeat.base.dao.LabelDao; +import org.apache.hertzbeat.manager.service.impl.LabelServiceImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; + +/** + * Test case for {@link LabelService} + */ +@ExtendWith(MockitoExtension.class) +class LabelServiceTest { + + @InjectMocks + private LabelServiceImpl labelService; + + @Mock + private LabelDao labelDao; + + @Test + void addLabel() { + // Prepare test data + Label label = Label.builder().id(1L).name("tagname").tagValue("tagvalue").build(); + when(labelDao.findLabelByNameAndTagValue(anyString(), anyString())).thenReturn(Optional.empty()); + + labelService.addLabel(label); + + verify(labelDao).save(label); + + } + + @Test + void modifyLabel() { + Label tag = Label.builder().id(1L).build(); + when(labelDao.findById(1L)).thenReturn(Optional.of(tag)); + when(labelDao.save(tag)).thenReturn(tag); + assertDoesNotThrow(() -> labelService.modifyLabel(tag)); + reset(); + when(labelDao.findById(1L)).thenReturn(Optional.empty()); + assertThrows(IllegalArgumentException.class, () -> labelService.modifyLabel(tag)); + } + + @Test + void getLabels() { + when(labelDao.findAll(any(Specification.class), any(PageRequest.class))).thenReturn(Page.empty()); + assertNotNull(labelService.getLabels(null, null, 1, 10)); + } + + @Test + void deleteLabels() { + assertDoesNotThrow(() -> labelService.deleteLabels(new HashSet<>(1))); + } + + @Test + void determineNewLabels() { + List